首页资源分类嵌入式开发单片机 > 零死角玩转STM32—F429

零死角玩转STM32—F429

已有 456470个资源

下载专区

文档信息举报收藏

标    签: STM32F429M4野火

分    享:

文档简介

零死角玩转STM32—F429

野火出品

文档预览

零死角玩转 STM32—F429 第1章 如何使用本书 1.1 本书的参考资料 本书参考资料为:《STM32F4xx 中文参考手册》和《Cortex®-M4 内核编程手册》, 这两本是 ST 官方的手册,属于精华版,面面俱到,无所不包。限于篇幅问题,本书不可 能面面具到,着重框图分析和代码讲解,有关寄存器的详细描述则略过,在学习本书的时 候,涉及到寄存器描述部分还请参考这两本手册,这样学习效果会更佳。 1.2 本书的编写风格 本书着重讲解 F429 的外设以及外设的应用,力争全面分析每个外设的功能框图和外设 的使用方法,让读者可以零死角的玩转 STM32—F429。基本每个章节对应一个外设,每章 的主要内容大概分为三个部分,第一部分为简介,第二部分为外设功能框图分析,第三部 分为代码讲解。 外设简介则是用自己的话把外设概括性的介绍一遍,力图语句简短,通俗易懂,并不 会完全照抄数据手册的介绍。 外设功能框图分析则是章节的重点,该部分会详细讲解功能框图的每个部分的作用, 这是学习 F429 的精髓所在,掌握了整个外设的框图则可以熟练的使用该外设,熟练的编程, 日后学习其他型号的单片机,也将会得心应手。因为即使单片机的型号不同,外设的框图 还是基本一样的。这一步的学习比较枯燥,但是必须死磕,方能达成所愿。 代码分析则是讲解使用该外设的实验讲解,主要分析代码流程,和一些编程的注意事 项。在掌握了框图之后,代码部分则是手到擒来而已。 1.3 本书的配套硬件 本书配套的硬件平台为:秉火 STM32-F429 至尊版,学习的时候如果配套该硬件平台 做实验,学习必会达到事半功倍的效果,可以省去中间移植时遇到的各种问题。 第 1 页 共 928 零死角玩转 STM32—F429 图 1 秉火 STM32—F429 至尊版硬件资源 1.4 本书的技术论坛 如果在学习过程中遇到问题,可以到论坛:www.chuxue123.com 发帖交流 ,开源共享, 共同进步。 鉴于水平有限,本书难免有纰漏,热心的读者也可把勘误发到论坛好让我们改进做得 更好,祝您学习愉快,M4 的世界,秉火与您同行。 第 2 页 共 928 零死角玩转 STM32—F429 第2章 如何安装 KEIL5 本章内容所涉及的软件只供教学使用,不得用于商业用途。个人或公司如需商用请购 买正版。 2.1 温馨提示 1、安装路径不能带中文,必须是英文路径 2、安装目录不能跟 51 的 KEIL 或者 KEIL4 冲突,三者目录必须分开 3、KEIL5 的安装比起 KEIL4 多了一个步骤,必须添加 MCU 库,不然没法使用。 4、如果使用的时候出现莫名其妙的错误,先百度查找解决方法,莫乱阵脚。 2.2 获取 KEIL5 要想获得 KEIL5 的安装包,在百度里面搜索“KEIL5 下载”即可找到很多网友提供的 下载文件,或者到 KEIL 的官网下载:https://www.keil.com/download/product/,一大堆注册 非常麻烦。我们这里面 KEIL5 的版本是 MDK5.15,以后有新版本大家可使用更高版本。 2.3 开始安装 双击 KEIL5 安装包,开始安装,next。 第 3 页 共 928 零死角玩转 STM32—F429 Agree,Nest 选择安装 路径,路径不能带中文,next 第 4 页 共 928 零死角玩转 STM32—F429 填写用户信息,全部填空格(键盘的 space 键)即可,next Finish,安装完毕 第 5 页 共 928 零死角玩转 STM32—F429 2.4 安装 MCU Device 包 KEIL5 不像 KEIL4 那样自带了很多厂商的 MCU 型号,KEIL5 需要自己安装。 把下面弹出的界面关掉,我们直接去 keil 的官网下载:http://www.keil.com/dd2/pack/,或者 直接用我们下载好的包。 在官网中找到 STM32F1、STM32F4、STM32F7 这 3 个系列的包下载到本地电脑,具 体下载哪个系列的根据你使用的型号下载即可,这里我只下载我自己需要使用的 F1/4/7 这 三个系列的包,F1 代表 M3,F4 代表 M4,F7 代表 M7。 第 6 页 共 928 零死角玩转 STM32—F429 把下载好的包双击安装即可,安装路径选择跟 KEIL5 一样的安装路径,安装成功之后, 在 KEIL5 的 Pack Installer 中就可以看到我们安装的包,以后我们新建工程的时候,就有单 片机的型号可选。 第3章 如何用 DAP 仿真器下载程序 3.1 仿真器简介 本书配套的仿真器为 Fire-Debugger,遵循 ARM 公司的 CMSIS-DAP 标准,支持所有 基于 Cortex 内核的单片机,常见的 M3、M4 和 M7 都可以完美支持。 第 7 页 共 928 零死角玩转 STM32—F429 Fire-Debugger 支持下载和在线仿真程序,支持 XP/WIN7/WIN8/WIN10 这四个操作系 统,免驱,不需要安装驱动即可使用,支持 KEIL 和 IAR 直接下载,非常方便。 3.2 硬件连接 把仿真器用 USB 线连接电脑,如果仿真器的灯亮则表示正常,可以使用。然后把仿真 器的另外一端连接到开发板,给开发板上电,然后就可以通过软件 KEIL 或者 IAR 给开发 板下载程序。 图 3-1 仿真器与电脑和开发板连接方式 3.3 仿真器配置 在仿真器连接好电脑和开发板且开发板供电正常的情况下,打开编译软件 KEIL,在魔 术棒选项卡里面选择仿真器的型号,具体过程看图示: 1. Debug 选项配置 图 3-2 Debug 选择 CMSIS-DAP Debugger 第 8 页 共 928 零死角玩转 STM32—F429 2. Utilities 选项配置 图 3-3 Utilities 选择 Use Debug Driver 3. Debug Settings 选项配置 图 3-4 Debug Settings 选项配置 第 9 页 共 928 零死角玩转 STM32—F429 3.4 选择目标板 选择目标板,具体选择多大的 FLASH 要根据板子上的芯片型号决定。秉火 STM32 开 发板的配置是:F1 选 512K,F4 选 1M。这里面有个小技巧就是把 Reset and Run 也勾选上, 这样程序下载完之后就会自动运行,否则需要手动复位。擦除的 FLASH 大小选择 Sectors 即可,不要选择 Full Chip,不然下载会比较慢。 图 3-5 选择目标板 3.5 下载程序 如果前面步骤都成功了,接下来就可以把编译好的程序下载到开发板上运行。下载程 序不需要其他额外的软件,直接点击 KEIL 中的 LOAD 按钮即可。 图 3-6 下载程序 程序下载后,Build Output 选项卡如果打印出 Application running…则表示程序下载成 功。如果没有出现实验现象,按复位键试试。 第 10 页 共 928 零死角玩转 STM32—F429 图 3-7 程序运行成功 第 11 页 共 928 零死角玩转 STM32—F429 第4章 初识 STM32 本章参考资料:1、《STM8 和 STM32 产品选型手册》2、SetupSTM32CubeMX-4.11.0.exe 4.1 什么是 STM32 STM32,从字面上来理解,ST 是意法半导体,M 是 Microelectronics 的缩写,32 表示 32 位,合起来理解,STM32 就是指 ST 公司开发的 32 位微控制器。在如今的 32 位控制器 当中,STM32 可以说是最璀璨的新星,它受宠若娇,大受工程师和市场的青睐,无芯能出 其右。 4.1.1 STM32 诞生的背景 51 是嵌入式学习中一款入门级的精典 MCU,因其结构简单,易于教学,且可以通过 串口编程而不需要额外的仿真器,所以在教学时被大量采用,至今很多大学在嵌入式教学 中用的还是 51。51 诞生于 70 年代,属于传统的 8 位单片机,如今,久经岁月的洗礼,既 有其辉煌又有其不足。现在的市场产品竞争越来越激烈,对成本极其敏感,相应地对 MCU 的 性能要求也更苛刻:更多功能,更低功耗,易用界面和多任务。面对这些要求,51 现有 的 资源就显得得抓襟见肘。所以无论是高校教学还是市场需求,都急需一款新的 MCU 来 为这个领域注入新的活力。 基于这样的市场需求, ARM 公司推出了其全新的基于 ARMv7 架构的 32 位 CortexM3 微控制器内核。紧随其后,ST(意法半导体)公司就推出了基于 Cortex-M3 内核的 MCU—STM32。STM32 凭借其产品线的多样化、极高的性价比、简单易用的库开发方 式, 迅速在众多 Cortex-M3 MCU 中脱颖而出,成为最闪亮的一颗新星。STM32 一上市就 迅速 占领了中低端 MCU 市场,受到了市场和工程师的无比青睐,颇有星火燎原之势。 作为一名合格的嵌入式工程师,面对新出现的技术,我们不是充耳不闻,而是要尽快 吻合市场的需要,跟上技术的潮流。如今 STM32 的出现就是一种趋势,一种潮流,我们 要 做的就是搭上这趟快车,让自己的技术更有竞争力。 4.2 STM32 能做什么 STM32 属于一个微控制器,自带了各种常用通信接口,比如 USART、I2C、SPI 等, 可接非常多的传感器,可以控制很多的设备。现实生活中,我们接触到的很多电器产品都 有 STM32 的身影,比如智能手环,微型四轴飞行器,平衡车、移动 POST 机,智能电饭锅, 3D 打印机等等。下面我们以最近最为火爆的两个产品来讲解下,一个是手环,一个是飞行 器。 第 12 页 共 928 零死角玩转 STM32—F429 4.2.1 智能手环 图 4-1 三星 GearFit 智能手环 红圈:STM32F439ZIY6S 处理器,2048KB FLASH ,256KB RAM ,WLCSP143 封装。 橙圈:Macronix MX69V28F64 16 MB 闪存,基于 MCP 封装的存储器,是一种包含了 NOR 和 SRAM 的闪存,这在手环手机这种移动设备中经常使用,优点是体积小,可以减 小 PCB 的尺寸。这个闪存用的 439 的 FSMC 接口驱动。 黄圈:InvenSense MPU-6500 陀螺仪/加速度计,用 439 的 I2C 接口驱动。 绿圈:博通 BCM4334WKUBG 芯片,支持 802.11n,蓝牙 4.0+HS 以及 FM 接收芯片, 用 439 的 SDIO 或者 SPI 接口驱动。 显示:1.84"可弯曲屏幕(Super AMOLED),432 x 128 像素。触摸部分用 439 的 I2C 接 口驱动,OLED 显示部分用 LTDC 接口驱动。 表格 4-1 三星 Gear Fit 和秉火 STM32F429 至尊版资源对比 资源 三星 Gear Fit 秉火 STM32F429 至尊版 CPU STM32F439ZIY6S , WLCSP143 封 STM32F429IGT6,LQPF144 封装 装 存储 显示 NOR+SRAM 16MB,FSMC 接口 SDRAM 8MB,FMC 接口 1.84 寸的 AMOLED,RGB 接口, 5 寸电容屏,RGB 接口,LTDC LTDC 驱动 驱动 陀螺仪 MPU6050,I2C 接口 MPU6050,I2C 接口 无线通信 蓝 牙 : 博 通 BCM4334 , SDIO 或 者 WIFI:美满 W8782,SDIO 接口 SPI 接口 除了这几个重要资源的对比,我们的 429 开发板上还集成了以太网,音频,CAN, 485,232,USB 转串口,蜂鸣器,LED,电容按键等外设资源,可以充分的学习 429 这个 芯片。在板子上面,还可以跑系统 ucosiii,学习图形界面 emwin。如果功夫所至,学完之 后,自己都可以做一个类似 Gear Fit 这样的手环。可很多人又会说,Gear Fit 涉及硬件和软 件,整个系统这么复杂,并不是一个人可以完成的。说的没错,我们可以做不了,但是我 们的能力可以无限接近,多学点,技多不压身嘛。 第 13 页 共 928 零死角玩转 STM32—F429 图 4-2 ucosiii+emwin 做的系统界面(429 开发板的开机界面) 4.2.2 微型四轴飞行器 现在无人机非常火热,高端的无人机用 STM32 做不来,但是小型的四轴飞行器用 STM32 还是绰绰有余的。如图 4-3 所示飞行器的基本都可以用 STM32 搞定。 图 4-3 微型四轴飞行器 上面的是属于产品,如果想自己 DIY,可以在入门 STM32 之后,买一本飞行器 DIY 的书,边做边学。入门级的书籍推荐《四轴飞行器 DIY—基于 STM32 微控制器》,见图 4-4。 第 14 页 共 928 零死角玩转 STM32—F429 图 4-4 四轴飞行器 DIY —基于 STM32 微控制器 4.2.3 淘宝众筹 学会了 STM32,想自己做产品,如何实现自己的梦想,淘宝众筹吧。做出产品原型, 用别人的钱为自己的梦想买单。 淘宝众筹科技类网址:这里面有很多小玩意都可以用 STM32 实现,只要你的创意到了, 就会有人买单,前提是我们要先学会 STM32。 https://hi.taobao.com/market/hi/list.php?spm=a215p.1596646.1.8.LbVyJk#type=121288001 图 4-5 淘宝众筹科技类 4.3 STM32 怎么选型 4.3.1 STM32 分类 STM32 有很多系列,可以满足市场的各种需求,从内核上分有 Cortex-M0、M3、M4 和 M7 这几种,每个内核又大概分为主流、高性能和低功耗。具体的见表格 4-2。 第 15 页 共 928 零死角玩转 STM32—F429 单纯从学习的角度出发,可以选择 F1 和 F4,F1 代表了基础型,基于 Cortex-M3 内核, 主频为 72MHZ,F4 代表了高性能,基于 Cortex-M4 内核,主频 180M。 之于 F1,F4(429 系列以上)除了内核不同和主频的提升外,升级的明显特色就是带 了 LCD 控制器和摄像头接口,支持 SDRAM,这个区别在项目选型上会被优先考虑。 表格 4-2 STM8 和 STM32 分类 CPU 位数 内核 Cortex-M0 Cortex-M3 32 Cortex-M4 Cortex-M7 8 超级版 6502 系列 STM32-F0 STM32-L0 STM32-F1 STM32-F2 STM32-L1 STM32-F3 STM32-F4 STM32-L4 STM32-F7 STM8S STM8AF STM8AL STM8L 描述 入门级 低功耗 基础型,主频 72M 高性能 低功耗 混和信号 高性能,主频 180M 低功耗 高性能 标准系列 标准系列的汽车应用 低功耗的汽车应用 低功耗 4.3.2 STM32 命名方法 这里我们以秉火 F429 至尊版用的型号 STM32F429IGT6 来讲解下 STM32 的命名方法。 表格 4-3 STM32F429IGT6 命名解释 — 家族 产品类型 具体特性 引脚数目 FLASH 大小 封装 温度 ST M32 F 429 I G T 6 STM32 表示 32bit 的 MCU F 表示基础型 429 表示高性能且带 DSP 和 FPU I 表示 176pin,其他常用的为 C 表示 48,R 表示 64,V 表示 100,Z 表示 144,B 表示 208,N 表示 216 G 表示 1024KB,其他常用的为 C 表示 256,E 表示 512,I 表示 2048 T 表示 QFP 封装,这个是最常用的封装 6 表示温度等级为 A :-40~85° 有关更详细的命名方法见图 4-6。 第 16 页 共 928 零死角玩转 STM32—F429 图 4-6 STM8 和 STM32 命名方法,摘自《STM8 和 STM32 选型手册》 4.3.3 选择合适的 MCU 了解了 STM32 的分类和命名方法之后,就可以根据项目的具体需求先大概选择哪类内 核的 MCU,普通应用,不需要接大屏幕的一般选择 Cortex-M3 内核的 F1 系列,如果要追 求高性能,需要大量的数据运算,且需要外接 RGB 大屏幕的则选择 Cortex-M4 内核的 F429 系列。 明确了大方向之后,接下来就是细分选型,先确定引脚,引脚多的功能就多,价格也 贵,具体得根据实际项目中需要使用到什么功能,够用就好。确定好了引脚数目之后再选 择 FLASH 大小,相同引脚数的 MCU 会有不同的 FLASH 大小可供选择,这个也是根据实 际需要选择,程序大的就选择大点的 FLASH,要是产品一量产,这些省下来的都是钱啊。 有些月出货量以 KK(百万数量级)为单位的产品,不仅是 MCU,连电阻电容能少用就少 用,更甚者连 PCB 的过孔的多少都有讲究。项目中的元器件的选型的水深着啊,很多学问。 1. 如何分配原理图 IO 在画原理图之前,一般的做法是先把引脚分类好,然后才开始画原理图,引脚分类具 体见表格 4-4。 表格 4-4 画原理图时的引脚分类 引脚分类 引脚说明说明 电源 (VBAT)、(VDD VSS)、(VDDA VSSA)、(VREF+ VREF-)等 晶振 IO 下载 IO BOOT IO 主晶振 IO,RTC 晶振 IO 用于 JTAG 下载的 IO:JTMS、JTCK、JTDI、JTDO、NJTRST BOOT0、BOOT1,用于设置系统的启动方式 复位 IO GPIO NRST,用于外部复位 上面 5 部分 IO 组成的系统我们也叫做最小系统 专用器件接到专用的总线,比如 I2C,SPI,SDIO,FSMC,DCMI 这些总线 的器件需要接到专用的 IO 第 17 页 共 928 零死角玩转 STM32—F429 普通的元器件接到 GPIO,比如蜂鸣器,LED,按键等元器件用普通的 GPIO 即 如果还有剩下的 IO,可根据项目需要引出或者不引出 2. 如何寻找 IO 的功能说明 要想根据功能来分配 IO,那就得先知道每个 IO 的功能说明,这个我们可以从官方的 数据手册里面找到。在学习的时候,有两个官方资料我们会经常用到,一个是参考手册 (英文叫 Reference manual),另外一个是数据手册(英文叫 Data Sheet)。两者的具体区 别见表格 4-5。 表格 4-5 参考手册和数据手册的内容区别 手册 主要内容 说明 参考手册 片上外设的功能说 明和寄存器描述 对片上每一个外设的功能和使用做了详细的说明,包含 寄存器的详细描述。编程的时候需要反复查询这个手 册。 功能概览 主要讲这个芯片有哪些功能,属于概括性的介绍。芯片 选型的时候首先看这个部分。 引脚说明 详细描述每一个引脚的功能,设计原理图的时候和写程 数据手册 内存映射 序的时候需要参考这部分。 讲解该芯片的内存映射,列举每个总线的地址和包含有 哪些外设。 封装特性 讲解芯片的封装,包含每个引脚的长度宽度等,我们画 PCB 封装的时候需要参考这部分的参数。 一句话概括:数据手册主要用于芯片选型和设计原理图时参考,参考手册主要用于在 编程的时候查阅。官方的这两个文档可以从官方网址里面下载: http://www.stmcu.org/document/list/index/category-150,也可以从我们配置的光盘资料里面找 到。 在数据手册中,有关引脚定义的部分在 Pinouts and pin description 这个小节中,具体定 义见表格 4-6。 表格 4-6 数据手册中对引脚定义 表格 4-7 对引脚定义的解读 名称 缩写 说明 ① 引脚序号 阿拉伯数字表示 LQFP 封装,英文字母开头的表示 BGA 封装。引脚序号 第 18 页 共 928 零死角玩转 STM32—F429 ② 引脚名称 ③ 引脚类型 ④ I/O 结构 ⑤ 注意事项 ⑥ 复用功能 ⑦ 额外功能 这里列出了有 8 种封装型号,具体使用哪一种要根据实际情况来选择。 指复位状态下的引脚名称 S 电源引脚 I 输入引脚 I/O 输入/输出引脚 FT 兼容 5V TTa 只支持 3V3,且直接到 ADC B BOOT 引脚 RST 复位引脚,内部带弱上拉 对某些 IO 要注意的事项的特别说明 IO 的复用功能,过 GPIOx_AFR 寄存器来配置选择。一个 IO 口可以复用 为多个功能,即一脚多用,这个在设计原理图和编程的时候要灵活选择。 IO 的额外功能,通过直连的外设寄存器配置来选择。个人觉得在使用上跟 复用功能差不多。 3. 开始分配原理图 IO 比如我们的 F429 至尊版使用的 MCU 型号是 STM32F429IGT6,封装为 LQFP176,我 们在数据手册中找到这个封装的引脚定义,然后根据引脚序号,一个一个复制出来,整理 成 excel 表。具体整理方法按照表格 4-4 画原理图时的引脚分类即可。分配好之后就开始画 原理图。 4.3.4 PCB 哪里打样 设计好原理图,画好 PCB 之后,需要把板子做出来,进行软硬件联调。首先得 PCB 打样,这里我推荐一家我经常打样的厂家,深圳嘉立创(JLC),行业标杆,良心价格, 网址:http://www.sz-jlc.com。一块 10CM*10CM 以内的板子,三天做好,50 块就可以搞定, 还包邮,简直便宜到掉渣。如果你足够懒,不想自己焊接电阻电容二三极管什么的,嘉立 创还可以帮你把 PCB 样板上的阻容贴好给你,打样贴片一条龙。 样品做好了,软硬件什么都 OK,要小批量怎么办?还是找 JLC。 第 19 页 共 928 零死角玩转 STM32—F429 第5章 什么是寄存器 本章参考资料:《STM32F4xx 中文参考手册》、《STM32F429xx 数据手册》、 学习本章时,配合《STM32F4xx 中文参考手册》“存储器和总线架构”、“嵌入式 FLASH 接口”及“通用 I/O(GPIO)”章节一起阅读,效果会更佳,特别是涉及到寄存器说 明的部分。 5.1 什么是寄存器 我们经常说寄存器,那么什么是寄存器?这是我们本章需要讲解的内容,在学习的过 程中,大家带着这个疑问好好思考下,到最后看看大家能否用一句话给寄存器下一个定义。 5.2 STM32 长啥样 我们开发板中使用的芯片是 176pin 的 STM32F429IGT6,具体见图 5-1。这个就是我们 接下来要学习的 STM32,它讲带领我们进入嵌入式的殿堂。 芯片正面是丝印,ARM 应该是表示该芯片使用的是 ARM 的内核,STM32F429IGT6 是芯片型号,后面的字应该是跟生产批次相关,最下面的是 ST 的 LOGO。 芯片四周是引脚,左下角的小圆点表示 1 脚,然后从 1 脚起按照逆时针的顺序排列 (所有芯片的引脚顺序都是逆时针排列的)。开发板中把芯片的引脚引出来,连接到各种 传感器上,然后在 STM32 上编程(实际就是通过程序控制这些引脚输出高电平或者低电平) 来控制各种传感器工作,通过做实验的方式来学习 STM32 芯片的各个资源。开发板是一种 评估板,板载资源非常丰富,引脚复用比较多,力求在一个板子上验证芯片的全部功能。 图 5-1 STM32F429IGT6 实物图 第 20 页 共 928 零死角玩转 STM32—F429 图 5-2 STM32F429IGT6 正面引脚图 5.3 芯片里面有什么 我们看到的 STM32 芯片已经是已经封装好的成品,主要由内核和片上外设组成。若与 电脑类比,内核与外设就如同电脑上的 CPU 与主板、内存、显卡、硬盘的关系。 STM32F429 采用的是 Cortex-M4 内核,内核即 CPU,由 ARM 公司设计。ARM 公司 并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如 ST、TI、Freescale,负 责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。 如 GPIO、USART(串口)、I2C、SPI 等都叫做片上外设。具体见图 5-3。 第 21 页 共 928 零死角玩转 STM32—F429 图 5-3 STM32 芯片架构简图 芯片和外设之间通过各种总线连接,其中主控总线有 8 条,被控总线有 7 条,具体见 图 5-4。主控总线通过一个总线矩阵来连接被控总线,总线矩阵用于主控总线之间的访问仲 裁管理,仲裁采用循环调度算法。总线之间交叉的时候如果有个圆圈则表示可以通信,没有圆 圈则表示不可以通信。比如 S0:I 总线只有跟 M0、M2 和 M6 这三根被控总线交叉的时候才有 圆圈,就表示 S0 只能跟这三根被控总线通信。从功能上来理解,I 总线是指令总线,用来取指, 指令指的是编译好的程序指令。我们知道 STM32 有三种启动方式,从 FLASH 启动(包含系统 存储器),从内部 SRAM 启动,从外部 RAM 启动,这三种存储器刚好对应的就是 M0、M2 和 M6 这三条总线。 图 5-4 STM32F42xxx 和 STM32F43xxx 器件的总线接口 第 22 页 共 928 零死角玩转 STM32—F429 5.4 存储器映射 在图 5-4 中,连接被控总线的是 FLASH,RAM 和片上外设,这些功能部件共同排列 在一个 4GB 的地址空间内。我们在编程的时候,操作的也正是这些功能部件。 5.4.1 存储器映射 存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址 的过程就称为存储器映射,具体见图 5-5。如果给存储器再分配一个地址就叫存储器重映 射。 图 5-5 存储器映射 第 23 页 共 928 零死角玩转 STM32—F429 1. 存储器区域功能划分 在这 4GB 的地址空间中,ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个 块也都规定了用途,具体分类见表格 5-1。每个块的大小都有 512MB,显然这是非常大的, 芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的 一部分而已。 表格 5-1 存储器功能分类 序号 用途 地址范围 Block 0 Block 1 Block 2 SRAM SRAM 片上外设 0x0000 0000 ~ 0x1FFF FFFF(512MB) 0x2000 0000 ~ 0x3FFF FFFF(512MB) 0x4000 0000 ~ 0x5FFF FFFF(512MB) Block 3 FMC 的 bank1 ~ bank2 Block 4 FMC 的 bank3 ~ bank4 0x6000 0000 ~ 0x7FFF FFFF(512MB) 0x8000 0000 ~ 0x9FFF FFFF(512MB) Block 5 Block 6 Block 7 FMC FMC Cortex-M4 内部外设 0xA000 0000 ~ 0xCFFF FFFF(512MB) 0xD000 0000 ~ 0xDFFF FFFF(512MB) 0xE000 0000 ~ 0xFFFF FFFF(512MB) 在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。Boock0 用来设 计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设,下面我 们简单的介绍下这三个 Block 里面的具体区域的功能划分。 存储器 Block0 内部区域功能划分 Block0 主要用于设计片内的 FLASH, F429 系列片内部 FLASH 最大是 2MB,我们使 用的 STM32F429IGT6 的 FLASH 是 1MB。要在芯片内部集成更大的 FLASH 或者 SRAM 都意味着芯片成本的增加,往往片内集成的 FLASH 都不会太大,ST 能在追求性价比的同 时做到 1MB 以上,实乃良心之举。Block 内部区域的功能划分具体见表格 5-2。 表格 5-2 存储器 Block0 内部区域功能划分 块 用途说明 地址范围 Block0 预留 OTP 区域:其中 512 个字节只能写 一次,用于存储用户数据,额外的 16 个字节用于锁定对应的 OTP 数据 块。 预留 系统存储器:里面寸的是 ST 出厂时 烧写好的 isp 自举程序,用户无法改 动。串口下载的时候需要用到这部 分程序。 预留 选项字节:用于配置读写保护、 BOR 级别、软件/硬件看门狗以及器 件处于待机或停止模式下的复位。当 芯片不小心被锁住之后,我们可以从 RAM 里面启动来修改这部分相应的寄 存器位。 预留 CCM 数据 RAM:64K,CPU 直接 0x1FFF C008 ~ 0x1FFF FFFF 0x1FFF C000 ~ 0x1FFF C00F 0x1FFF 7A10 ~ 0x1FFF 7FFF 0x1FFF 0000 ~ 0x1FFF 7A0F 0x1FFE C008 ~ 0x1FFE FFFF 0x1FFE C000 ~ 0x1FFE C0FF 0x1001 0000 ~ 0x1FFE BFFF 0x1000 0000 ~ 0x1000 FFFF 第 24 页 共 928 零死角玩转 STM32—F429 通过 D 总线读取,不用经过总线矩 阵,属于高速的 RAM。 预留 FLASH:我们的程序就放在这里。 预留 取决于 BOOT 引脚,为 FLASH、系 统存储器、SRAM 的别名。 储存器 Block1 内部区域功能划分 0x0820 0000 ~ 0x000F FFFF 0x0800 0000 ~ 0x081F FFFF (2MB) 0x0020 0000 ~ 0x07FF FFFF 0x0000 0000 ~ 0x001F FFFF Block1 用于设计片内的 SRAM。F429 内部 SRAM 的大小为 256KB,其中 64KB 的 CCM RAM 位于 Block0,剩下的 192KB 位于 Block1,分 SRAM1 112KB,SRAM2 16KB, SRAM3 64KB,Block 内部区域的功能划分具体见表格 5-3。 表格 5-3 存储器 Block1 内部区域功能划分 块 用途说明 地址范围 预留 0x2003 0000 ~ 0x3FFF FFFF Block1 SRAM3 64KB SRAM2 16KB SRAM1 112KB 储存器 Block2 内部区域功能划分 0x2002 0000 ~ 0x2002 FFFF 0x2001 C000~ 0x2001 FFFF 0x2000 0000 ~0x2001 BFFFF Block2 用于设计片内的外设,根据外设的总线速度不同,Block 被分成了 APB 和 AHB 两部分,其中 APB 又被分为 APB1 和 APB2,AHB 分为 AHB1 和 AHB2,具体见表格 5-4。 还有一个 AHB3 包含了 Block3/4/5/6,这四个 Block 用于扩展外部存储器,如 SDRAM, NORFLASH 和 NANDFLASH 等。 表格 5-4 存储器 Block2 内部区域功能划分 块 Block2 用途说明 APB1 总线外设 预留 APB2 总线外设 预留 AHB1 总线外设 预留 AHB2 总线外设 预留 地址范围 0x4000 0000 ~ 0x4000 7FFF 0x4000 8000 ~ 0x4000 FFFF 0x4001 0000 ~ 0x4001 6BFF 0x4001 6C00 ~ 0x4001 FFFF 0x4002 0000 ~ 0x4007 FFFF 0x4008 0000 ~ 0x4FFF FFFF 0x5000 0000 ~ 0x5006 0BFF 0x5006 0C00 ~ 0x5FFF FFFF 5.5 寄存器映射 我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射,那什么叫 寄存器映射?寄存器到底是什么? 在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可 以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次 都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元 功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器, 这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。 第 25 页 共 928 零死角玩转 STM32—F429 比如,我们找到 GPIOH 端口的输出数据寄存器 ODR 的地址是 0x4002 1C14(至于这 个地址如何找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit,低 16bit 有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指 针的操作方式,让 GPIOH 的 16 个 IO 都输出高电平,具体见代码 5-1。 代码 5-1 通过绝对地址访问内存单元 1 // GPIOH 端口全部输出 高电平 2 *(unsigned int*)(0x4002 1C14) = 0xFFFF; 0x4002 1C14 在我们看来是 GPIOH 端口 ODR 的地址,但是在编译器看来,这只是一 个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换, 把它转换成指针,即(unsigned int *)0x4002 1C14,然后再对这个指针进行 * 操作。 刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存 器的方式来操作,具体见代码 5-2。 代码 5-2 通过寄存器别名方式访问内存单元 1 // GPIOH 端口全部输出 高电平 2 #define GPIOH_ODR 3 * GPIOH_ODR = 0xFF; (unsigned int*)(GPIOH_BASE+0x14) 为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面,具体见代码 5-3。 代码 5-3 通过寄存器别名访问内存单元 1 // GPIOH 端口全部输出 高电平 2 #define GPIOH_ODR 3 GPIOH_ODR = 0xFF; *(unsigned int*)(GPIOH_BASE+0x14) 5.5.1 STM32 的外设地址映射 片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB 挂载低速外设,AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线 基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从 这里开始,也叫外设基地址。 1. 总线基地址 表格 5-5 总线基地址 总线名称 总线基地址 相对外设基地址的偏移 APB1 APB2 AHB1 AHB2 AHB3 0x4000 0000 0x4001 0000 0x4002 0000 0x5000 0000 0x6000 0000 0x0 0x0001 0000 0x0002 0000 0x1000 0000 已不属于片上外设 表格 5-5 的“相对外设基地址偏移”即该总线地址与“片上外设”基地址 0x4000 0000 的差值。关于地址的偏移我们后面还会讲到。 2. 外设基地址 总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为 “XX 外设基地址”,也叫 XX 外设的边界地址。具体有关 STM32F4xx 外设的边界地址请 第 26 页 共 928 零死角玩转 STM32—F429 参考《STM32F4xx 参考手册》的 2.3 小节的存储器映射的表 2:STM32F4xx 寄存器边界地 址。或者参考《STM32F4xx 参考手册》的存储器映射章节,这两个手册都有详细的讲解。 这里面我们以 GPIO 这个外设来讲解外设的基地址,具体见表格 5-6。 表格 5-6 外设 GPIO 基地址 外设名称 外设基地址 相对 AHB1 总线的地址偏移 GPIOA 0x4002 0000 0x0 GPIOB 0x4002 0400 0x0000 0400 GPIOC 0x4002 0800 0x0000 0800 GPIOD 0x4002 0C00 0x0000 0C00 GPIOE 0x4002 1000 0x0000 1000 GPIOF 0x4002 1400 0x0000 1400 GPIOG 0x4002 1800 0x0000 1800 GPIOH 0x4002 1C00 0x0000 1C00 从表格 5-6 看到,GPIOA 的基址相对于 AHB1 总线的地址偏移为 0,我们应该就可以 猜到,AHB1 总线的第一个外设就是 GPIOA。 3. 外设寄存器 在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例,GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输 出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极,LED 灯的 阳极接电源,然后通过 STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭。 GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit,占四个字节, 在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描 述。这里我们以 GPIOH 端口为例,来说明 GPIO 都有哪些寄存器,具体见表格 5-7。 表格 5-7 GPIOH 端口的 寄存器地址列表 寄存器名称 寄存器地址 相对 GPIOH 基址的偏移 GPIOH_MODER 0x4002 1C00 0x00 GPIOH_OTYPER 0x4002 1C04 0x04 GPIOH_OSPEEDR 0x4002 1C08 0x08 GPIOH_PUPDR GPIOH_IDR 0x4002 1C0C 0x4002 1C10 0x0C 0x10 GPIOH_ODR 0x4002 1C14 0x14 GPIOH_BSRR 0x4002 1C18 0x18 GPIOH_LCKR 0x4002 1C1C 0x1C GPIOH_AFRL 0x4002 1C20 0x20 GPIOH_AFRH 0x4002 1C24 0x24 有关外设的寄存器说明可参考《STM32F4xx 参考手册》中具体章节的寄存器描述部分, 在编程的时候我们需要反复的查阅外设的寄存器说明。 这里我们以“GPIO 端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具 体见图 5-6。 第 27 页 共 928 零死角玩转 STM32—F429 图 5-6 GPIO 端口置位/复位寄存器说明  ①名称 寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思 是该寄存器名为“GPIOx_BSRR”其中的“x”可以为 A-I,也就是说这个寄存器说明适用 于 GPIOA、GPIOB 至 GPIOI,这些 GPIO 端口都有这样的一个寄存器。  ②偏移地址 偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x18, 从参考手册中我们可以查到 GPIOA 外设的基地址为 0x4002 0000 ,我们就可以算出 GPIOA 的这个 GPIOA_BSRR 寄存器的地址为:0x4002 0000+0x18 ;同理,由于 GPIOB 的 外设基地址为 0x4002 0400,可算出 GPIOB_BSRR 寄存器的地址为:0x4002 0400+0x18 。 其他 GPIO 端口以此类推即可。  ③寄存器位表 紧接着的是本寄存器的位表,表中列出它的 0-31 位的名称及权限。表上方的数字为位 编号,中间为位名称,最下方为读写权限,其中 w 表示只写,r 表示只读,rw 表示可读写。 本寄存器中的位权限都是 w,所以只能写,如果读本寄存器,是无法保证读取到它真正内 容的。而有的寄存器位只读,一般是用于表示 STM32 外设的某种工作状态的,由 STM32 硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。  ④位功能说明 位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本 寄存器中有两种寄存器位,分别为 BRy 及 BSy,其中的 y 数值可以是 0-15,这里的 0-15 第 28 页 共 928 零死角玩转 STM32—F429 表示端口的引脚号,如 BR0、BS0 用于控制 GPIOx 的第 0 个引脚,若 x 表示 GPIOA,那就 是控制 GPIOA 的第 0 引脚,而 BR1、BS1 就是控制 GPIOA 第 1 个引脚。 其中 BRy 引脚的说明是“0:不会对相应的 ODRx 位执行任何操作;1:对相应 ODRx 位进行复位”。这里的“复位”是将该位设置为 0 的意思,而“置位”表示将该位设置为 1;说明中的 ODRx 是另一个寄存器的寄存器位,我们只需要知道 ODRx 位为 1 的时候, 对应的引脚 x 输出高电平,为 0 的时候对应的引脚输出低电平即可(感兴趣的读者可以查询 该寄存器 GPIOx_ODR 的说明了解)。所以,如果对 BR0 写入“1”的话,那么 GPIOx 的第 0 个引脚就会输出“低电平”,但是对 BR0 写入“0”的话,却不会影响 ODR0 位,所以引 脚电平不会改变。要想该引脚输出“高电平”,就需要对“BS0”位写入“1”,寄存器位 BSy 与 BRy 是相反的操作。 5.5.2 C 语言对寄存器的封装 以上所有的关于存储器映射的内容,最终都是为大家更好地理解如何用 C 语言控制读 写外设寄存器做准备,此处是本章的重点内容。 1. 封装总线和外设基地址 在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起 来,总线或者外设都以他们的名字作为宏名,具体见代码 5-4。 代码 5-4 总线和外设基址宏定义 1 /* 外设基地址 */ 2 #define PERIPH_BASE ((unsigned int)0x40000000) 3 4 /* 总线基地址 */ 5 #define APB1PERIPH_BASE PERIPH_BASE 6 #define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000) 7 #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000) 8 #define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000) 9 10 /* GPIO 外设基地址 */ 11 #define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000) 12 #define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400) 13 #define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800) 14 #define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00) 15 #define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000) 16 #define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400) 17 #define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800) 18 #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00) 19 20 /* 寄存器基地址,以 GPIOH 为例 */ 21 #define GPIOH_MODER (GPIOH_BASE+0x00) 22 #define GPIOH_OTYPER (GPIOH_BASE+0x04) 23 #define GPIOH_OSPEEDR (GPIOH_BASE+0x08) 24 #define GPIOH_PUPDR (GPIOH_BASE+0x0C) 25 #define GPIOH_IDR (GPIOH_BASE+0x10) 26 #define GPIOH_ODR (GPIOH_BASE+0x14) 27 #define GPIOH_BSRR (GPIOH_BASE+0x18) 28 #define GPIOH_LCKR (GPIOH_BASE+0x1C) 29 #define GPIOH_AFRL (GPIOH_BASE+0x20) 30 #define GPIOH_AFRH (GPIOH_BASE+0x24) 第 29 页 共 928 零死角玩转 STM32—F429 代码 5-4 首先定义了 “片上外设”基地址 PERIPH_BASE,接着在 PERIPH_BASE 上 加入各个总线的地址偏移,得到 APB1、APB2 等总线的地址 APB1PERIPH_BASE、 APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到 GPIOA、GPIOH 的外设地址, 最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址, 就可以用指针操作读写了,具体见代码 5-5。 代码 5-5 使用指针控制 BSRR 寄存器 1 /* 控制 GPIOH 引脚 10 输出低电平(BSRR 寄存器的 BR10 置 1) */ 2 *(unsigned int *)GPIOH_BSRR = (0x01<<(16+10)); 3 4 /* 控制 GPIOH 引脚 10 输出高电平(BSRR 寄存器的 BS10 置 1) */ 5 *(unsigned int *)GPIOH_BSRR = 0x01<<10; 6 7 unsigned int temp; 8 /* 控制 GPIOH 端口所有引脚的电平(读 IDR 寄存器) */ 9 temp = *(unsigned int *)GPIOH_IDR; 该代码使用 (unsigned int *) 把 GPIOH_BSRR 宏的数值强制转换成了地址,然后再用 “*”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也 是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32 外设的状态。 2. 封装寄存器列表 用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOH 都各有一组功能相同 的寄存器,如 GPIOA_MODER/GPIOB_MODER/GPIOC_MODER 等等,它们只是地址不一 样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中 的结构体语法对寄存器进行封装,具体见代码 5-6。 代码 5-6 使用结构体对 GPIO 寄存器组的封装 1 typedef unsigned int uint32_t; /*无符号 32 位变量*/ 2 typedef unsigned short int uint16_t; /*无符号 16 位变量*/ 3 4 /* GPIO 寄存器列表 */ 5 typedef struct { 6 uint32_t MODER; /*GPIO 模式寄存器 地址偏移: 0x00 */ 7 uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */ 8 uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */ 9 uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */ 10 uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */ 11 uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */ 12 uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */ 13 uint16_t BSRRH; /*GPIO 置位/复位寄存器高 16 位部分 地址偏移: 0x1A */ 14 uint32_t LCKR; /*GPIO 配置锁定寄存器 地址偏移: 0x1C */ 15 uint32_t AFR[2]; /*GPIO 复用功能配置寄存器 地址偏移: 0x20-0x24 */ 16 } GPIO_TypeDef; 这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 8 个 成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间 是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节,具体见图 5-7。 第 30 页 共 928 零死角玩转 STM32—F429 寄存器(结构体成员变量) 偏移量 MODER(32 位) 0x00 GPIO_TypeDef 结构体 OTYPER(32 位) OSPEEDR(32 位) PUPDR(32 位) IDR(32 位) 0x04 0x08 0x0c 0x10 ODR(32 位) 0x14 … … 图 5-7 GPIO_TypeDef 结构体成员的地址偏移 也就是说,我们定义的这个 GPIO_TypeDef ,假如这个结构体的首地址为 0x4002 1C00(这也是第一个成员变量 MODER 的地址), 那么结构体中第二个成员变量 OTYPER 的地址即为 0x4002 1C00 +0x04 ,加上的这个 0x04 ,正是代表 MODER 所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释 已给出,其中的 BSRR 寄存器分成了低 16 位 BSRRL 和高 16 位 BSRRH,BSRRL 置 1 引脚 输出高电平,BSRRH 置 1 引脚输出低电平,这里分开只是为了方便操作。 这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体 设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存 器了,具体见代码 5-7。 代码 5-7 通过结构体指针访问寄存器 1 GPIO_TypeDef * GPIOx; 2 GPIOx = GPIOH_BASE; 3 GPIOx->BSRRL = 0xFFFF; 4 GPIOx->MODER = 0xFFFFFFFF; 5 GPIOx->OTYPER =0xFFFFFFFF; 6 7 uint32_t temp; 8 temp = GPIOx->IDR; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx //把指针地址设置为宏 GPIOH_BASE 地址 //通过指针访问并修改 GPIOH_BSRRL 寄存器 //修改 GPIOH_MODER 寄存器 //修改 GPIOH_OTYPER 寄存器 //读取 GPIOH_IDR 寄存器的值到变量 temp 中 这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx,并让指针指向地址 GPIOH_BASE(0x4002 1C00),使用地址确定下来,然后根据 C 语言访问结构体的语法,用 GPIOx->BSRRL、GPIOx->MODER 及 GPIOx->IDR 等方式读写寄存器。 最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向 各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可,具体代码 5-8。 代码 5-8 定义好 GPIO 端口首地址址针 1 /*使用 GPIO_TypeDef 把地址强制转换成指针*/ 2 #define GPIOA 3 #define GPIOB ((GPIO_TypeDef *) GPIOA_BASE) ((GPIO_TypeDef *) GPIOB_BASE) 第 31 页 共 928 零死角玩转 STM32—F429 4 #define GPIOC 5 #define GPIOD 6 #define GPIOE 7 #define GPIOF 8 #define GPIOG 9 #define GPIOH 10 11 12 13 /*使用定义好的宏直接访问*/ 14 /*访问 GPIOH 端口的寄存器*/ 15 GPIOH->BSRRL = 0xFFFF; 16 GPIOH->MODER = 0xFFFFFFF; 17 GPIOH->OTYPER =0xFFFFFFF; 18 19 uint32_t temp; 20 temp = GPIOH->IDR; 21 22 /*访问 GPIOA 端口的寄存器*/ 23 GPIOA->BSRRL = 0xFFFF; 24 GPIOA->MODER = 0xFFFFFFF; 25 GPIOA->OTYPER =0xFFFFFFF; 26 27 uint32_t temp; 28 temp = GPIOA->IDR; ((GPIO_TypeDef *) GPIOC_BASE) ((GPIO_TypeDef *) GPIOD_BASE) ((GPIO_TypeDef *) GPIOE_BASE) ((GPIO_TypeDef *) GPIOF_BASE) ((GPIO_TypeDef *) GPIOG_BASE) ((GPIO_TypeDef *) GPIOH_BASE) //通过指针访问并修改 GPIOH_BSRRL 寄存器 //修改 GPIOH_MODER 寄存器 //修改 GPIOH_OTYPER 寄存器 //读取 GPIOH_IDR 寄存器的值到变量 temp 中 //通过指针访问并修改 GPIOA_BSRRL 寄存器 //修改 GPIOA_MODER 寄存器 //修改 GPIOA_OTYPER 寄存器 //读取 GPIOA_IDR 寄存器的值到变量 temp 中 这里我们仅是以 GPIO 这个外设为例,给大家讲解了 C 语言对寄存器的封装。以此类 推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完 成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。 5.6 每课一问 1、 什么是存储器映射?什么是存储器重映射? 2、 什么是寄存器? 第 32 页 共 928 零死角玩转 STM32—F429 第6章 新建工程—寄存器版 本章内容所涉及的软件只供教学使用,不得用于商业用途。个人或公司因商业用途导 致的法律责任,后果自负。 版本说明:MDK5.15 版本号可从 MDK 软件的“Help-->About uVision”选项中查询到。 6.1 新建工程 6.1.1 新建本地工程文件夹 为了工程目录更加清晰,我们在本地电脑上新建 1 个文件夹用于存放整个工程,如命 名为“LED”,然后在该目录下新建 2 个文件夹,具体如下: 表格 8 工程目录文件夹清单 名称 Listing Output 作用 存放编译器编译时候产生的 c/汇编/链接的列表清单 存放编译产生的调试信息、hex 文件、预览信息、封装库等 图 6-1 工程文件夹目录 在本地新建好文件夹后,在文件夹下新建一些文件: 表格 9 工程目录文件夹内容清单 名称 作用 LED Listing 存放 startup_stm32f429_439xx.s、stm32f4xx.h、main.c 文件 暂时为空 第 33 页 共 928 零死角玩转 STM32—F429 Output 暂时为空 6.1.2 新建工程 打开 KEIL5,新建一个工程,工程名根据喜好命名,我这里取 LED-REG,直接保存 在 LED 文件夹下。 图 6-2 在 KEIL5 中新建工程 1. 选择 CPU 型号 这个根据你开发板使用的 CPU 具体的型号来选择, M4 至尊版选 STM32F429IGT 型号。 如果这里没有出现你想要的 CPU 型号,或者一个型号都没有,那么肯定是你的 KEIL5 没 有添加 device 库,KEIL5 不像 KEIL4 那样自带了很多 MCU 的型号,KEIL5 需要自己添加, 关于如何添加请参考《如何安装 KEIL5》这一章。 图 6-3 选择具体的 CPU 型号 第 34 页 共 928 零死角玩转 STM32—F429 2. 在线添加库文件 用寄存器控制 STM32 时我们不需要在线添加库文件,这里我们点击关掉。 图 6-4 库文件管理 3. 添加文件 在新建的工程中添加文件,文件从本地建好的工程文件夹下获取,双击组文件夹就会 出现添加文件的路径,然后选择文件即可。 图 6-5 如何在工程中添加文件 第 35 页 共 928 零死角玩转 STM32—F429 4. 配置魔术棒选项卡 这一步的配置工作很重要,很多人串口用不了 printf 函数,编译有问题,下载有问题, 都是这个步骤的配置出了错。 a) Target 中选中微库“ Use MicroLib”,为的是在日后编写串口驱动的时候可以 使用 printf 函数。而且有些应用中如果用了 STM32 的浮点运算单元 FPU,一 定要同时开微库,不然有时会出现各种奇怪的现象。FPU 的开关选项在微库配 置选项下方的“Use Single Precision”中,默认是开的。 图 6-6 添加微库 b) Output 选项卡中把输出文件夹定位到我们工程目录下的 output 文件夹,如果想 在编译的过程中生成 hex 文件,那么那 Create HEX File 选项勾上。 第 36 页 共 928 零死角玩转 STM32—F429 图 6-7 配置 Output 选项卡 ③在 Listing 选项卡中把输出文件夹定位到我们工程目录下的 Listing 文件夹。 图 6-8 配置 Listing 选项卡 第 37 页 共 928 零死角玩转 STM32—F429 5. 仿真器配置 在仿真器连接好电脑和开发板且开发板供电正常的情况下,打开编译软件 KEIL,在魔 术棒选项卡里面选择仿真器的型号,具体过程看图示: Debug 选项配置 Utilities 选项配置 图 6-9 Debug 选择 CMSIS-DAP Debugger 图 6-10 Utilities 选择 Use Debug Driver 第 38 页 共 928 零死角玩转 STM32—F429 Debug Settings 选项配置 图 6-11 Debug Settings 选项配置 6. 选择 CPU 型号 选择目标板,具体选择多大的 FLASH 要根据板子上的芯片型号决定。秉火 STM32 开 发板的配置是:F1 选 512K,F4 选 1M。这里面有个小技巧就是把 Reset and Run 也勾选上, 这样程序下载完之后就会自动运行,否则需要手动复位。擦除的 FLASH 大小选择 Sectors 即可,不要选择 Full Chip,不然下载会比较慢。 第 39 页 共 928 零死角玩转 STM32—F429 图 6-12 选择目标板 一个新的工程模版建立完毕。 第 40 页 共 928 零死角玩转 STM32—F429 第7章 使用寄存器点亮 LED 灯 本章参考资料:《STM32F4xx 中文参考手册》、《STM32F429 规格书》。 学习本章时,配合《STM32F4xx 中文参考手册》 “通用 I/O(GPIO)”章节一起阅读, 效果会更佳,特别是涉及到寄存器说明的部分。关于建立工程时使用 KEIL5 的基本操作, 请参考前面的章节。 7.1 GPIO 简介 GPIO 是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,STM32 芯片 的 GPIO 引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。 STM32 芯片的 GPIO 被分成很多组,每组有 16 个引脚,如型号为 STM32F4IGT6 型号的芯 片有 GPIOA、GPIOB、GPIOC 至 GPIOI 共 9 组 GPIO,芯片一共 176 个引脚,其中 GPIO 就占了一大部分,所有的 GPIO 引脚都有基本的输入输出功能。 最基本的输出功能是由 STM32 控制引脚输出高、低电平,实现开关控制,如把 GPIO 引脚接入到 LED 灯,那就可以控制 LED 灯的亮灭,引脚接入到继电器或三极管,那就可 以通过继电器或三极管控制外部大功率电路的通断。 最基本的输入功能是检测外部输入电平,如把 GPIO 引脚连接到按键,通过电平高低 区分按键是否被按下。 7.2 GPIO 框图剖析 图 7-1 GPIO 结构框图 第 41 页 共 928 零死角玩转 STM32—F429 通过 GPIO 硬件结构框图,就可以从整体上深入了解 GPIO 外设及它的各种应用模式。 该图从最右端看起,最右端就是代表 STM32 芯片引出的 GPIO 引脚,其余部件都位于芯片 内部。 7.2.1 基本结构分析 下面我们按图中的编号对 GPIO 端口的结构部件进行说明。 1. 保护二极管及上、下拉电阻 引脚的两保护个二级管可以防止引脚外部过高或过低的电压输入,当引脚电压高于 VDD_FT 时,上方的二极管导通,当引脚电压低于 VSS 时,下方的二极管导通,防止不正常 电压引入芯片导致芯片烧毁。尽管有这样的保护,并不意味着 STM32 的引脚能直接外接大 功率驱动器件,如直接驱动电机,强制驱动要么电机不转,要么导致芯片烧坏,必须要加 大功率及隔离电路驱动。具体电压、电流范围可查阅《STM32F4xx 规格书》。 上拉、下拉电阻,从它的结构我们可以看出,通过上、下拉对应的开关配置,我们可 以控制引脚默认状态的电压,开启上拉的时候引脚电压为高电平,开启下拉的时候引脚电 压为低电平,这样可以消除引脚不定状态的影响。如引脚外部没有外接器件,或者外部的 器件不干扰该引脚电压时,STM32 的引脚都会有这个默认状态。 也可以设置“既不上拉也不下拉模式”,我们也把这种状态称为浮空模式,配置成这 个模式时,直接用电压表测量其引脚电压为 1 点几伏,这是个不确定值。所以一般来说我 们都会选择给引脚设置“上拉模式”或“下拉模式”使它有默认状态。 STM32 的内部上拉是“弱上拉”,即通过此上拉输出的电流是很弱的,如要求大电流 还是需要外部上拉。 通过“上拉/下拉寄存器 GPIOx_PUPDR”控制引脚的上、下拉以及浮空模式。 2. P-MOS 管和 N-MOS 管 GPIO 引脚线路经过上、下拉电阻结构后,向上流向“输入模式”结构,向下流向“输 出模式”结构。先看输出模式部分,线路经过一个由 P-MOS 和 N-MOS 管组成的单元电路。 这个结构使 GPIO 具有了“推挽输出”和“开漏输出”两种模式。 所谓的推挽输出模式,是根据这两个 MOS 管的工作方式来命名的。在该结构中输入 高电平时,上方的 P-MOS 导通,下方的 N-MOS 关闭,对外输出高电平;而在该结构中输 入低电平时,N-MOS 管导通,P-MOS 关闭,对外输出低电平。当引脚高低电平切换时, 两个管子轮流导通,一个负责灌电流,一个负责拉电流,使其负载能力和开关速度都比普 通的方式有很大的提高。推挽输出的低电平为 0 伏,高电平为 3.3 伏,参考图 7-2 左侧,它 是推挽输出模式时的等效电路。 第 42 页 共 928 零死角玩转 STM32—F429 图 7-2 等效电路 而在开漏输出模式时,上方的 P-MOS 管完全不工作。如果我们控制输出为 0,低电平, 则 P-MOS 管关闭,N-MOS 管导通,使输出接地,若控制输出为 1 (它无法直接输出高电平) 时,则 P-MOS 管和 N-MOS 管都关闭,所以引脚既不输出高电平,也不输出低电平,为高 阻态。为正常使用时必须接上拉电阻(可用 STM32 的内部上拉,但建议在 STM32 外部再接 一个上拉电阻),参考图 7-2 中的右侧等效电路。它具“线与”特性,也就是说,若有很多 个开漏模式引脚连接到一起时,只有当所有引脚都输出高阻态,才由上拉电阻提供高电平, 此高电平的电压为外部上拉电阻所接的电源的电压。若其中一个引脚为低电平,那线路就 相当于短路接地,使得整条线路都为低电平,0 伏。 推挽输出模式一般应用在输出电平为 0 和 3.3 伏而且需要高速切换开关状态的场合。 在 STM32 的应用中,除了必须用开漏模式的场合,我们都习惯使用推挽输出模式。 开漏输出一般应用在 I2C、SMBUS 通讯等需要“线与”功能的总线电路中。除此之外, 还用在电平不匹配的场合,如需要输出 5 伏的高电平,就可以在外部接一个上拉电阻,上 拉电源为 5 伏,并且把 GPIO 设置为开漏模式,当输出高阻态时,由上拉电阻和电源向外 输出 5 伏的电平。 通过 “输出类型寄存器 GPIOx_OTYPER”可以控制 GPIO 端口是推挽模式还是开漏模 式。 3. 输出数据寄存器 前面提到的双 MOS 管结构电路的输入信号,是由 GPIO“输出数据寄存器 GPIOx_ODR”提供的,因此我们通过修改输出数据寄存器的值就可以修改 GPIO 引脚的输 出电平。而“置位/复位寄存器 GPIOx_BSRR”可以通过修改输出数据寄存器的值从而影响 电路的输出。 第 43 页 共 928 零死角玩转 STM32—F429 4. 复用功能输出 “复用功能输出”中的“复用”是指 STM32 的其它片上外设对 GPIO 引脚进行控制, 此时 GPIO 引脚用作该外设功能的一部分,算是第二用途。从其它外设引出来的“复用功 能输出信号”与 GPIO 本身的数据据寄存器都连接到双 MOS 管结构的输入中,通过图中的 梯形结构作为开关切换选择。 例如我们使用 USART 串口通讯时,需要用到某个 GPIO 引脚作为通讯发送引脚,这个 时候就可以把该 GPIO 引脚配置成 USART 串口复用功能,由串口外设控制该引脚,发送数 据。 5. 输入数据寄存器 看 GPIO 结构框图的上半部分,它是 GPIO 引脚经过上、下拉电阻后引入的,它连接 到施密特触发器,信号经过触发器后,模拟信号转化为 0、1 的数字信号,然后存储在“输 入数据寄存器 GPIOx_IDR”中,通过读取该寄存器就可以了解 GPIO 引脚的电平状态。 6. 复用功能输入 与“复用功能输出”模式类似,在“复用功能输出模式”时,GPIO 引脚的信号传输到 STM32 其它片上外设,由该外设读取引脚状态。 同样,如我们使用 USART 串口通讯时,需要用到某个 GPIO 引脚作为通讯接收引脚, 这个时候就可以把该 GPIO 引脚配置成 USART 串口复用功能,使 USART 可以通过该通讯 引脚的接收远端数据。 7. 模拟输入输出 当 GPIO 引脚用于 ADC 采集电压的输入通道时,用作“模拟输入”功能,此时信号是 不经过施密特触发器的,因为经过施密特触发器后信号只有 0、1 两种状态,所以 ADC 外 设要采集到原始的模拟信号,信号源输入必须在施密特触发器之前。类似地,当 GPIO 引 脚用于 DAC 作为模拟电压输出通道时,此时作为“模拟输出”功能,DAC 的模拟信号输 出就不经过双 MOS 管结构了,在 GPIO 结构框图的右下角处,模拟信号直接输出到引脚。 同时,当 GPIO 用于模拟功能时(包括输入输出),引脚的上、下拉电阻是不起作用的,这个 时候即使在寄存器配置了上拉或下拉模式,也不会影响到模拟信号的输入输出。 7.2.2 GPIO 工作模式 总结一下,由 GPIO 的结构决定了 GPIO 可以配置成以下模式: 1. 输入模式(上拉/下拉/浮空) 在输入模式时,施密特触发器打开,输出被禁止。数据寄存器每隔 1 个 AHB1 时钟周 期更新一次,可通过输入数据寄存器 GPIOx_IDR 读取 I/O 状态。其中 AHB1 的时钟如按默 认配置一般为 180MHz。 第 44 页 共 928 零死角玩转 STM32—F429 用于输入模式时,可设置为上拉、下拉或浮空模式。 2. 输出模式(推挽/开漏,上拉/下拉) 在输出模式中,输出使能,推挽模式时双 MOS 管以方式工作,输出数据寄存器 GPIOx_ODR 可控制 I/O 输出高低电平。开漏模式时,只有 N-MOS 管工作,输出数据寄存 器可控制 I/O 输出高阻态或低电平。输出速度可配置,有 2MHz\25MHz\50MHz\100MHz 的 选项。此处的输出速度即 I/O 支持的高低电平状态最高切换频率,支持的频率越高,功耗 越大,如果功耗要求不严格,把速度设置成最大即可。 此时施密特触发器是打开的,即输入可用,通过输入数据寄存器 GPIOx_IDR 可读取 I/O 的实际状态。 用于输出模式时,可使用上拉、下拉模式或浮空模式。但此时由于输出模式时引脚电 平会受到 ODR 寄存器影响,而 ODR 寄存器对应引脚的位为 0,即引脚初始化后默认输出 低电平,所以在这种情况下,上拉只起到小幅提高输出电流能力,但不会影响引脚的默认 状态。 3. 复用功能(推挽/开漏,上拉/下拉) 复用功能模式中,输出使能,输出速度可配置,可工作在开漏及推挽模式,但是输出 信号源于其它外设,输出数据寄存器 GPIOx_ODR 无效;输入可用,通过输入数据寄存器 可获取 I/O 实际状态,但一般直接用外设的寄存器来获取该数据信号。 用于复用功能时,可使用上拉、下拉模式或浮空模式。同输出模式,在这种情况下, 初始化后引脚默认输出低电平,上拉只起到小幅提高输出电流能力,但不会影响引脚的默 认状态。 4. 模拟输入输出 模拟输入输出模式中,双 MOS 管结构被关闭,施密特触发器停用,上/下拉也被禁止。 其它外设通过模拟通道进行输入输出。 通过对 GPIO 寄存器写入不同的参数,就可以改变 GPIO 的应用模式,再强调一下, 要了解具体寄存器时一定要查阅《STM32F4xx 参考手册》中对应外设的寄存器说明。在 GPIO 外设中,通过设置“模式寄存器 GPIOx_MODER”可配置 GPIO 的输入/输出/复用/模 拟模式,“输出类型寄存器 GPIOx_OTYPER”配置推挽/开漏模式,配置“输出速度寄存 器 GPIOx_OSPEEDR”可选 2/25/50/100MHz 输出速度,“上/下拉寄存器 GPIOx_PUPDR” 可配置上拉/下拉/浮空模式,各寄存器的具体参数值见表 7-1。 第 45 页 共 928 零死角玩转 STM32—F429 表 7-1 GPIO 寄存器的参数配置 模式寄存器的 MODER 位[0:1] 01 -输出模式 10 -复用模式 00 -输入模式 11 -模拟功能 输出类型寄存器的 OTYPER 位 0 -推挽模式 1 -开漏模式 不可用 不可用 输出速度寄存器的 OSPEEDR 00 -速度 2MHz 01 -速度 25MHz 10 -速度 50MHz 11 -速度 100MHz 不可用 不可用 上/下拉寄存器的 PUPDR 位[0:1] 00 -无上拉无下 拉 01 -上拉 10 -下拉 11 -保留 00 -无上拉无下 拉 01 -保留 10 -保留 11 -保留 7.3 实验:使用寄存器点亮 LED 灯 本小节中,我们以实例讲解如何通过控制寄存器来点亮 LED 灯。此处侧重于讲解原理, 请您直接用 KEIL5 软件打开我们提供的实验例程配合阅读,先了解原理,学习完本小节后, 再尝试自己建立一个同样的工程。本节配套例程名称为“GPIO 输出—寄存器点亮 LED 灯”,在工程目录下找到后缀为“.uvprojx”的文件,用 KEIL5 打开即可。 自己尝试新建工程时,请对照查阅《用 KEIL5 新建工程模版 寄存器版本》章节。 若没有安装 KEIL5 软件,请参考《如何安装 KEIL5》章节。 打开该工程,见图 7-3,可看到一共有三个文件,分别 startup_stm32f429_439xx.s 、 stm32f4xx.h 以及 main.c,下面我们对这三个工程进行讲解。 图 7-3 工程文件结构 7.3.1 硬件连接 在本教程中 STM32 芯片与 LED 灯的连接见图 7-4。 第 46 页 共 928 零死角玩转 STM32—F429 图 7-4 LED 灯电路连接图 图中从 3 个 LED 灯的阳极引出连接到 3.3V 电源,阴极各经过 1 个电阻引入至 STM32 的 3 个 GPIO 引脚 PH10、PH11、PH12 中,所以我们只要控制这三个引脚输出高低电平, 即可控制其所连接 LED 灯的亮灭。如果您的实验板 STM32 连接到 LED 灯的引脚或极性不 一样,只需要修改程序到对应的 GPIO 引脚即可,工作原理都是一样的。 我们的目标是把 GPIO 的引脚设置成推挽输出模式并且默认下拉,输出低电平,这样 就能让 LED 灯亮起来了。 7.3.2 启动文件 名为“startup_stm32f429_439xx.s”的文件,它里边使用汇编语言写好了基本程序,当 STM32 芯片上电启动的时候,首先会执行这里的汇编程序,从而建立起 C 语言的运行环境, 所以我们把这个文件称为启动文件。该文件使用的汇编指令是 Cortex-M4 内核支持的指令, 可从《Cortex-M4 Technical Reference Manual》查到,也可参考《Cortex-M3 权威指南中 文》,M3 跟 M4 大部分汇编指令相同。 startup_stm32f429_439xx.s 文件是由官方提供的,一般有需要也是在官方的基础上修改, 不会自己完全重写。该文件可以从 KEIL5 安装目录找到,也可以从 ST 库里面找到,找到 该文件后把启动文件添加到工程里面即可。不同型号的芯片以及不同编译环境下使用的汇 编文件是不一样的,但功能相同。 对于启动文件这部分我们主要总结它的功能,不详解讲解里面的代码,其功能如下:  初始化堆栈指针 SP;  初始化程序计数器指针 PC;  设置堆、栈的大小;  设置中断向量表的入口地址;  配置外部 SRAM 作为数据存储器(这个由用户配置,一般的开发板可没有外部 SRAM);  调用 SystemIni() 函数配置 STM32 的系统时钟。  设置 C 库的分支入口“__main”(最终用来调用 main 函数); 第 47 页 共 928 零死角玩转 STM32—F429 先去除繁枝细节,挑重点的讲,主要理解最后两点,在启动文件中有一段复位后立即 执行的程序,代码见代码清单 7-1。在实际工程中阅读时,可使用编辑器的搜索(Ctrl+F)功 能查找这段代码在文件中的位置。 代码清单 7-1 复位后执行的程序 1 ;Reset handler 2 Reset_Handler PROC 3 EXPORT Reset_Handler [WEAK] 4 IMPORT SystemInit 5 IMPORT __main 6 7 LDR R0, =SystemInit 8 BLX R0 9 LDR R0, =__main 10 BX R0 11 ENDP 开头的是程序注释,在汇编里面注释用的是“;”,相当于 C 语言的“//”注释符 第二行是定义了一个子程序:Reset_Handler。PROC 是子程序定义伪指令。这里就相 当于 C 语言里定义了一个函数,函数名为 Reset_Handler。 第三行 EXPORT 表示 Reset_Handler 这个子程序可供其他模块调用。相当于 C 语言的 函数声明。关键字[WEAK] 表示弱定义,如果编译器发现在别处定义了同名的函数,则在 链接时用别处的地址进行链接,如果其它地方没有定义,编译器也不报错,以此处地址进 行链接,如果不理解 WEAK,那就忽略它好了。 第四行和第五行 IMPORT 说明 SystemInit 和__main 这两个标号在其他文件,在链接的 时候需要到其他文件去寻找。相当于 C 语言中,从其它文件引入函数声明。以便下面对外 部函数进行调用。 SystemInit 需要由我们自己实现,即我们要编写一个具有该名称的函数,用来初始化 STM32 芯片的时钟,一般包括初始化 AHB、APB 等各总线的时钟,需要经过一系列的配 置 STM32 才能达到稳定运行的状态。 __main 其实不是我们定义的(不要与 C 语言中的 main 函数混淆),当编译器编译时,只 要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈、堆,配置系统 环境,准备好 C 语言并在最后跳转到用户自定义的 main 函数,从此来到 C 的世界。 第六行把 SystemInit 的地址加载到寄存器 R0。 第七行程序跳转到 R0 中的地址执行程序,即执行 SystemInit 函数的内容。 第八行把__main 的地址加载到寄存器 R0。 第九行程序跳转到 R0 中的地址执行程序,即执行__main 函数,执行完毕之后就去到 我们熟知的 C 世界,进入 main 函数。 第十行表示子程序的结束。 总之,看完这段代码后,了解到如下内容即可:我们需要在外部定义一个 SystemInit 函数设置 STM32 的时钟;STM32 上电后,会执行 SystemInit 函数,最后执行我们 C 语言 中的 main 函数。 第 48 页 共 928 零死角玩转 STM32—F429 7.3.3 stm32f4xx.h 文件 看完启动文件,那我们立即写 SystemInit 和 main 函数吧?别着急,定义好了 SystemInit 函数和 main 我们又能写什么内容?连接 LED 灯的 GPIO 引脚,是要通过读写寄 存器来控制的,就这样空着手,如何控制寄存器呢。在上一章,我们知道寄存器就是特殊 的内存空间,可以通过指针操作访问寄存器。所以此处我们根据 STM32 的存储分配先定义 好各个寄存器的地址,把这些地址定义都统一写在 stm32f4xx.h 文件中,见代码清单 7-2。 代码清单 7-2 外设地址定义 1 /*片上外设基地址 */ 2 #define PERIPH_BASE ((unsigned int)0x40000000) 3 /*总线基地址 */ 4 #define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000) 5 /*GPIO 外设基地址*/ 6 #define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00) 7 8 /* GPIOH 寄存器地址,强制转换成指针 */ 9 #define GPIOH_MODER *(unsigned int*)(GPIOH_BASE+0x00) 10 #define GPIOH_OTYPER *(unsigned int*)(GPIOH_BASE+0x04) 11 #define GPIOH_OSPEEDR *(unsigned int*)(GPIOH_BASE+0x08) 12 #define GPIOH_PUPDR *(unsigned int*)(GPIOH_BASE+0x0C) 13 #define GPIOH_IDR *(unsigned int*)(GPIOH_BASE+0x10) 14 #define GPIOH_ODR *(unsigned int*)(GPIOH_BASE+0x14) 15 #define GPIOH_BSRR *(unsigned int*)(GPIOH_BASE+0x18) 16 #define GPIOH_LCKR *(unsigned int*)(GPIOH_BASE+0x1C) 17 #define GPIOH_AFRL *(unsigned int*)(GPIOH_BASE+0x20) 18 #define GPIOH_AFRH *(unsigned int*)(GPIOH_BASE+0x24) 19 20 /*RCC 外设基地址*/ 21 #define RCC_BASE (AHB1PERIPH_BASE + 0x3800) 22 /*RCC 的 AHB1 时钟使能寄存器地址,强制转换成指针*/ 23 #define RCC_AHB1ENR *(unsigned int*)(RCC_BASE+0x30) GPIO 外设的地址跟上一章讲解的相同,不过此处把寄存器的地址值都直接强制转换成 了指针,方便使用。代码的最后两段是 RCC 外设寄存器的地址定义,RCC 外设是用来设 置时钟的,以后我们会详细分析,本实验中只要了解到使用 GPIO 外设必须开启它的时钟 即可。 7.3.4 main 文件 现在就可以开始编写程序了,在 main 文件中先编写一个 main 函数,里面什么都没有, 暂时为空。 1 int main (void) 2{ 3} 此时直接编译的话,会出现如下错误: “Error: L6218E: Undefined symbol SystemInit (referred from startup_stm32f429_439xx.o)” 错误提示 SystemInit 没有定义。从分析启动文件时我们知道,Reset_Handler 调用了该 函数用来初始化 SMT32 系统时钟,为了简单起见,我们在 main 文件里面定义一个 第 49 页 共 928 零死角玩转 STM32—F429 SystemInit 空函数,什么也不做,为的是骗过编译器,把这个错误去掉。关于配置系统时 钟我们在后面再写。当我们不配置系统时钟时,STM32 芯片会自动按系统内部的默认时钟 运行,程序还是能跑的。我们在 main 中添加如下函数: 1 // 函数为空,目的是为了骗过编译器不报错 2 void SystemInit(void) 3{ 4} 这时再编译就没有错了,完美解决。还有一个方法就是在启动文件中把有关 SystemInit 的代码注释掉也可以,见代码清单 7-3。 代码清单 7-3 注释掉启动文件中调用 SystemInit 的代码 1 ; Reset handler 2 Reset_Handler PROC 3 EXPORT Reset_Handler 4 ;IMPORT SystemInit 5 IMPORT __main 6 7 ;LDR R0, =SystemInit 8 ;BLX R0 9 LDR R0, =__main 10 BX R0 11 ENDP [WEAK] 接下来在 main 函数中添加代码,对寄存器进行控制,寄存器的控制参数可参考表 7-1(点击可跳转)或《STM32F4xx 参考手册》。 1. GPIO 模式 首先我们把连接到 LED 灯的 PH10 引脚配置成输出模式,即配置 GPIO 的 MODER 寄 存器,见图 7-5。MODER 中包含 0-15 号引脚,每个引脚占用 2 个寄存器位。这两个寄存 器位设置成“01”时即为 GPIO 的输出模式,见代码清单 7-4。 代码清单 7-4 配置输出模式 1 /*GPIOH MODER10 清空*/ 2 GPIOH_MODER &= ~( 0x03<< (2*10)); 3 /*PH10 MODER10 = 01b 输出模式*/ 4 GPIOH_MODER |= (1<<2*10); 第 50 页 共 928 零死角玩转 STM32—F429 图 7-5 MODER 寄存器说明(摘自《STM32F4xx 参考手册》) 在代码中,我们先把 GPIOH MODER 寄存器的 MODER10 对应位清 0,然后再向它赋 值“01”,从而使 GPIOH10 引脚设置成输出模式。 代码中使用了“&=~”、“|=”这种复杂位操作方法是为了避免影响到寄存器中的其 它位,因为寄存器不能按位读写,假如我们直接给 MODER 寄存器赋值: 1 GPIOH_MODER = 0x00100000; 这时 MODER10 的两个位被设置成“01”输出模式,但其它 GPIO 引脚就有意见了, 因为其它引脚的 MODER 位都已被设置成输入模式。 如果对此处“&=”“|=”这样的位操作方法还不理解,请阅读前面的《规范的位操作 方法》小节。熟悉这种方法之后,会发现这样按位操作其实比直接赋值还要直观。 2. 输出类型 GPIO 输出有推挽和开漏两种类型,我们了解到开漏类型不能直接输出高电平,要输出 高电平还要在芯片外部接上拉电阻,不符合我们的硬件设计,所以我们直接使用推挽模式。 配置 OTYPER 寄存中的 OTYPER10 寄存器位,该位设置为 0 时 PH10 引脚即为推挽模式, 见代码清单 7-5。 代码清单 7-5 设置为推挽模式 1 /*GPIOH OTYPER10 清空*/ 2 GPIOH_OTYPER &= ~(1<<1*10); 3 /*PH10 OTYPER10 = 0b 推挽模式*/ 4 GPIOH_OTYPER |= (0<<1*10); 第 51 页 共 928 零死角玩转 STM32—F429 3. 输出速度 GPIO 引脚的输出速度是引脚支持高低电平切换的最高频率,本实验可以随便设置。此 处我们配置 OSPEEDR 寄存器中的寄存器位 OSPEEDR10 即可控制 PH10 的输出速度,见代 码清单 7-6。 代码清单 7-6 设置输出速度为 2MHz 1 /*GPIOH OSPEEDR10 清空*/ 2 GPIOH_OSPEEDR &= ~(0x03<<2*10); 3 /*PH10 OSPEEDR10 = 0b 速率 2MHz*/ 4 GPIOH_OSPEEDR |= (0<<2*10); 4. 上/下拉模式 当 GPIO 引脚用于输入时,引脚的上/下拉模式可以控制引脚的默认状态。但现在我们 的 GPIO 引脚用于输出,引脚受 ODR 寄存器影响,ODR 寄存器对应引脚位初始初始化后 默认值为 0,引脚输出低电平,所以这时我们配置上/下拉模式都不会影响引脚电平状态。 但因此处上拉能小幅提高电流输出能力,我们配置它为上拉模式,即配置 PUPDR 寄存器 的 PUPDR10 位,设置为二进制值“01”,见代码清单 7-7。 代码清单 7-7 设置为下拉模式 1 /*GPIOH PUPDR10 清空*/ 2 GPIOH_PUPDR &= ~(0x03<<2*10); 3 /*PH10 PUPDR10 = 01b 下拉模式*/ 4 GPIOH_PUPDR |= (1<<2*10); 5. 控制引脚输出电平 在输出模式时,对 BSRR 寄存器和 ODR 寄存器写入参数即可控制引脚的电平状态。 简单起见,此处我们使用 BSRR 寄存器控制,对相应的 BR10 位设置为 1 时 PH10 即为低电 平,点亮 LED 灯,对它的 BS10 位设置为 1 时 PH10 即为高电平,关闭 LED 灯,见代码清 单 7-8。 代码清单 7-8 控制引脚输出电平 1 /*PH10 BSRR 寄存器的 BR10 置 1,使引脚输出低电平*/ 2 GPIOH_BSRR |= (1<<16<<10); 3 4 /*PH10 BSRR 寄存器的 BS10 置 1,使引脚输出高电平*/ 5 GPIOH_BSRR |= (1<<10); 6. 开启外设时钟 设置完 GPIO 的引脚,控制电平输出,以为现在总算可以点亮 LED 了吧,其实还差最 后一步。 在《STM32 芯片架构》的外设章节中提到 STM32 外设很多,为了降低功耗,每个外 设都对应着一个时钟,在芯片刚上电的时候这些时钟都是被关闭的,如果想要外设工作, 必须把相应的时钟打开。 第 52 页 共 928 零死角玩转 STM32—F429 STM32 的所有外设的时钟由一个专门的外设来管理,叫 RCC(reset and clockcontrol), RCC 在《 STM32 中文参考手册》的第六章。 所有的 GPIO 都挂载到 AHB1 总线上,所以它们的时钟由 AHB1 外设时钟使能寄存器 (RCC_AHB1ENR)来控制,其中 GPIOH 端口的时钟由该寄存器的位 7 写 1 使能,开启 GPIOH 端口时钟。以后我们还会详细解释 STM32 的时钟系统,此处我们了解到在访问 GPIO 的寄存器之前,要先使能它的时钟即可,使用代码清单 7-9 中的代码可以开启 GPIOH 时钟。 代码清单 7-9 开启端口时钟 1 /*开启 GPIOH 时钟,使用外设时都要先开启它的时钟*/ 2 RCC_AHB1ENR |= (1<<7); 7. 水到渠成 开启时钟,配置引脚模式,控制电平,经过这三步,我们总算可以控制一个 LED 了。 现在我们完整组织下用 STM32 控制一个 LED 的代码,见代码清单 7-10。 代码清单 7-10 main 文件中控制 LED 灯的代码 1 2 /* 3 使用寄存器的方法点亮 LED 灯 4 */ 5 #include "stm32f4xx.h" 6 7 8 /** 9 * 主函数 10 */ 11 int main(void) 12 { 13 /*开启 GPIOH 时钟,使用外设时都要先开启它的时钟*/ 14 RCC_AHB1ENR |= (1<<7); 15 16 /* LED 端口初始化 */ 17 18 /*GPIOH MODER10 清空*/ 19 GPIOH_MODER &= ~( 0x03<< (2*10)); 20 /*PH10 MODER10 = 01b 输出模式*/ 21 GPIOH_MODER |= (1<<2*10); 22 23 /*GPIOH OTYPER10 清空*/ 24 GPIOH_OTYPER &= ~(1<<1*10); 25 /*PH10 OTYPER10 = 0b 推挽模式*/ 26 GPIOH_OTYPER |= (0<<1*10); 27 28 /*GPIOH OSPEEDR10 清空*/ 29 GPIOH_OSPEEDR &= ~(0x03<<2*10); 30 /*PH10 OSPEEDR10 = 0b 速率 2MHz*/ 31 GPIOH_OSPEEDR |= (0<<2*10); 32 33 /*GPIOH PUPDR10 清空*/ 34 GPIOH_PUPDR &= ~(0x03<<2*10); 35 /*PH10 PUPDR10 = 01b 上拉模式*/ 36 GPIOH_PUPDR |= (1<<2*10); 37 第 53 页 共 928 零死角玩转 STM32—F429 38 /*PH10 BSRR 寄存器的 BR10 置 1,使引脚输出低电平*/ 39 GPIOH_BSRR |= (1<<16<<10); 40 41 /*PH10 BSRR 寄存器的 BS10 置 1,使引脚输出高电平*/ 42 //GPIOH_BSRR |= (1<<10); 43 44 while (1); 45 46 } 47 48 // 函数为空,目的是为了骗过编译器不报错 49 void SystemInit(void) 50 { 51 } 在本章节中,要求完全理解 stm32f4xx.h 文件及 main 文件的内容(RCC 相关的除外)。 7.3.1 下载验证 把编译好的程序下载到开发板并复位,可看到板子上的 LED 灯被点亮。 7.4 每课一问 1. 在《STM32F4xx 参考手册》中阅读 GPIO 章节中的各个寄存器说明。 2. 对照本章节的工程文件,自己建立一个同样的工程,可参考《用 KEIL5 新建工程模版 寄存器版本》、《如何安装 KEIL5》。 3. 修改本章节的工程文件,通过控制 ODR 寄存器点亮实验板上的灯。 4. 修改本章节的工程文件,点亮实验板上的其它 LED 灯。 5. 修改本章节的工程文件,控制 LED 灯闪烁,即循环亮、灭。(提示:在亮、灭控制之 间加延时,才能让肉眼看清闪烁)。 第 54 页 共 928 零死角玩转 STM32—F429 第8章 自己写库—构建库函数雏形 本章参考资料:《STM32F4xx 中文参考手册》、《STM32F429 规格书》 虽然我们上面用寄存器点亮了 LED,乍看一下好像代码也很简单,但是我们别侥幸以 后就可以一直用寄存器开发。在用寄存器点亮 LED 的时候,我们会发现 STM32 的寄存器 都是 32 位的,每次配置的时候都要对照着《STM32F4xx 参考手册》中寄存器的说明,然 后根据说明对每个控制的寄存器位写入特定参数,因此在配置的时候非常容易出错,而且 代码还很不好理解,不便于维护。所以学习 STM32 最好的方法是用软件库,然后在软件 库的基础上了解底层,学习遍所有寄存器。 8.1 什么是 STM32 函数库 以上所说的软件库是指“STM32 标准函数库”,它是由 ST 公司针对 STM32 提供的函 数接口,即 API (Application Program Interface),开发者可调用这些函数接口来配置 STM32 的寄存器,使开发人员得以脱离最底层的寄存器操作,有开发快速,易于阅读,维护成本 低等优点。 当我们调用库 API 的时候不需要挖空心思去了解库底层的寄存器操作,就像当年我们 刚开始学习 C 语言的时候,用 prinft()函数时只是学习它的使用格式,并没有去研究它的源 码实现,但需要深入研究的时候,经过千锤百炼的库 API 源码就是最佳学习范例。 实际上,库是架设在寄存器与用户驱动层之间的代码,向下处理与寄存器直接相关的 配置,向上为用户提供配置寄存器的接口。库开发方式与直接配置寄存器方式的区别见图 8-1。 驱动层 调用库接口 库函数层 以函数、宏封装配置 寄存器的操作 特殊寄存器层 驱动层 直接配置寄存器 特殊寄存器层 库开发方式 直接配置寄存器方式 图 8-1 开发方式对比图 第 55 页 共 928 零死角玩转 STM32—F429 8.2 为什么采用库来开发及学习? 在以前 8 位机时代的程序开发中,一般直接配置芯片的寄存器,控制芯片的工作方式, 如中断,定时器等。配置的时候,常常要查阅寄存器表,看用到哪些配置位,为了配置某 功能,该置 1 还是置 0。这些都是很琐碎的、机械的工作,因为 8 位机的软件相对来说较 简单,而且资源很有限,所以可以直接配置寄存器的方式来开发。 对于 STM32,因为外设资源丰富,带来的必然是寄存器的数量和复杂度的增加,这时 直接配置寄存器方式的缺陷就突显出来了: (1) 开发速度慢 (2) 程序可读性差 (3) 维护复杂 这些缺陷直接影响了开发效率,程序维护成本,交流成本。库开发方式则正好弥补了 这些缺陷。 而坚持采用直接配置寄存器的方式开发的程序员,会列举以下原因: (1) 具体参数更直观 (2) 程序运行占用资源少 相对于库开发的方式,直接配置寄存器方式生成的代码量的确会少一点,但因为 STM32 有充足的资源,权衡库的优势与不足,绝大部分时候,我们愿意牺牲一点 CPU 资 源,选择库开发。一般只有在对代码运行时间要求极苛刻的地方,才用直接配置寄存器的 方式代替,如频繁调用的中断服务函数。 对于库开发与直接配置寄存器的方式,就好比编程是用汇编好还是用 C 好一样。在 STM32F1 系列刚推出函数库时引起程序员的激烈争论,但是,随着 ST 库的完善与大家对 库的了解,更多的程序员选择了库开发。现在 STM32F1 系列和 STM32F4 系列各有一套自 己的函数库,但是它们大部分是兼容的,F1 和 F4 之间的程序移植,只需要小修改即可。 而如果要移植用寄存器写的程序,我只想说:“呵呵”。 用库来进行开发,市场已有定论,用户群说明了一切,但对于 STM32 的学习仍然有人 认为用寄存器好,而且汇编不是还没退出大学教材么?认为这种方法直观,能够了解到是 配置了哪些寄存器,怎样配置寄存器。事实上,库函数的底层实现恰恰是直接配置寄存器 方式的最佳例子,它代替我们完成了寄存器配置的工作,而想深入了解芯片是如何工作的 话,只要直接查看库函数的最底层实现就能理解,相信你会为它严谨、优美的实现方式而 陶醉,要想修炼 C 语言,就从 ST 的库开始吧。所以在以后的章节中,使用软件库是我们 的重点,而且我们通过讲解库 API 去高效地学习 STM32 的寄存器,并不至于因为用库学 习,就不会用寄存器控制 STM32 芯片。 8.3 实验:构建库函数雏形 虽然库的优点多多,但很多人对库还是很忌惮,因为一开始用库的时候有很多代码, 很多文件,不知道如何入手。不知道您是否认同这么一句话:一切的恐惧都来源于认知的 空缺。我们对库忌惮那是因为我们不知道什么是库,不知道库是怎么实现的。 第 56 页 共 928 零死角玩转 STM32—F429 接下来,我们在寄存器点亮 LED 的代码上继续完善,把代码一层层封装,实现库的最 初的雏形,相信经过这一步的学习后,您对库的运用会游刃有余。这里我们只讲如何实现 GPIO 函数库,其他外设的我们直接参考 ST 标准库学习即可,不必自己写。 下面请打开本章配套例程“构建库函数雏形”来阅读理解,该例程是在上一章的基础 上修改得来的。 8.3.1 修改寄存器地址封装 上一章中我们在操作寄存器的时候,操作的是都寄存器的绝对地址,如果每个外设寄 存器都这样操作,那将非常麻烦。我们考虑到外设寄存器的地址都是基于外设基地址的偏 移地址,都是在外设基地址上逐个连续递增的,每个寄存器占 32 个或者 16 个字节,这种 方式跟结构体里面的成员类似。所以我们可以定义一种外设结构体,结构体的地址等于外 设的基地址,结构体的成员等于寄存器,成员的排列顺序跟寄存器的顺序一样。这样我们 操作寄存器的时候就不用每次都找到绝对地址,只要知道外设的基地址就可以操作外设的 全部寄存器,即操作结构体的成员即可。 在工程中的“stm32f4xx.h”文件中,我们使用结构体封装 GPIO 及 RCC 外设的的寄存 器,见代码清单 8-1。结构体成员的顺序按照寄存器的偏移地址从低到高排列,成员类型 跟寄存器类型一样。如不理解 C 语言对寄存器的封的语法原理,请参考《C 语言对寄存器 的封装》小节。 代码清单 8-1 封装寄存器列表 1 //volatile 表示易变的变量,防止编译器优化 2 #define __IO volatile 3 typedef unsigned int uint32_t; 4 typedef unsigned short uint16_t; 5 6 /* GPIO 寄存器列表 */ 7 typedef struct { 8 __IO uint32_t MODER; /*GPIO 模式寄存器 地址偏移: 0x00 */ 9 __IO uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */ 10 __IO uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */ 11 __IO uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */ 12 __IO uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */ 13 __IO uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */ 14 __IO uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */ 15 __IO uint16_t BSRRH; /*GPIO 置位/复位寄存器 高 16 位部分地址偏移: 0x1A */ 16 __IO uint32_t LCKR; /*GPIO 配置锁定寄存器 地址偏移: 0x1C */ 17 __IO uint32_t AFR[2]; /*GPIO 复用功能配置寄存器 地址偏移: 0x20-0x24 */ 18 } GPIO_TypeDef; 19 20 /*RCC 寄存器列表*/ 21 typedef struct { 22 __IO uint32_t CR; /*!< RCC 时钟控制寄存器,地址偏移: 0x00 */ 23 __IO uint32_t PLLCFGR; /*!< RCC PLL 配置寄存器,地址偏移: 0x04 */ 24 __IO uint32_t CFGR; /*!< RCC 时钟配置寄存器,地址偏移: 0x08 */ 25 __IO uint32_t CIR; /*!< RCC 时钟中断寄存器,地址偏移: 0x0C */ 26 __IO uint32_t AHB1RSTR; /*!< RCC AHB1 外设复位寄存器,地址偏移: 0x10 */ 27 __IO uint32_t AHB2RSTR; /*!< RCC AHB2 外设复位寄存器,地址偏移: 0x14 */ 28 __IO uint32_t AHB3RSTR; /*!< RCC AHB3 外设复位寄存器,地址偏移: 0x18 */ 29 __IO uint32_t RESERVED0; /*!< 保留, 地址偏移:0x1C */ 30 __IO uint32_t APB1RSTR; /*!< RCC APB1 外设复位寄存器,地址偏移: 0x20 */ 第 57 页 共 928 零死角玩转 STM32—F429 31 __IO uint32_t APB2RSTR; /*!< RCC APB2 外设复位寄存器,地址偏移: 0x24*/ 32 __IO uint32_t RESERVED1[2]; /*!< 保留,地址偏移:0x28-0x2C*/ 33 __IO uint32_t AHB1ENR; /*!< RCC AHB1 外设时钟寄存器,地址偏移: 0x30 */ 34 __IO uint32_t AHB2ENR; /*!< RCC AHB2 外设时钟寄存器,地址偏移: 0x34 */ 35 __IO uint32_t AHB3ENR; /*!< RCC AHB3 外设时钟寄存器,地址偏移: 0x38 */ 36 /*RCC 后面还有很多寄存器,此处省略*/ 37 } RCC_TypeDef; 这段代码在每个结构体成员前增加了一个“__IO”前缀,它的原型在这段代码的第一 行,代表了 C 语言中的关键字“volatile”,在 C 语言中该关键字用于表示变量是易变的, 要求编译器不要优化。这些结构体内的成员,都代表着寄存器,而寄存器很多时候是由外 设或 STM32 芯片状态修改的,也就是说即使 CPU 不执行代码修改这些变量,变量的值也 有可能被外设修改、更新,所以每次使用这些变量的时候,我们都要求 CPU 去该变量的地 址重新访问。若没有这个关键字修饰,在某些情况下,编译器认为没有代码修改该变量, 就直接从 CPU 的某个缓存获取该变量值,这时可以加快执行速度,但该缓存中的是陈旧数 据,与我们要求的寄存器最新状态可能会有出入。 8.3.2 定义访问外设的结构体指针 以结构体的形式定义好了外设寄存器后,使用结构体前还需要给结构体的首地址赋值, 才能访问到需要的寄存器。为方便操作,我们给每个外设都定义好指向它地址的结构体指 针,见代码清单 8-2。 代码清单 8-2 指向外设首地址的结构体指针 1 /*定义 GPIOA-H 寄存器结构体指针*/ 2 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 3 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) 4 #define GPIOC ((GPIO_TypeDef *) GPIOC_BASE) 5 #define GPIOD ((GPIO_TypeDef *) GPIOD_BASE) 6 #define GPIOE ((GPIO_TypeDef *) GPIOE_BASE) 7 #define GPIOF ((GPIO_TypeDef *) GPIOF_BASE) 8 #define GPIOG ((GPIO_TypeDef *) GPIOG_BASE) 9 #define GPIOH ((GPIO_TypeDef *) GPIOH_BASE) 10 11 /*定义 RCC 外设 寄存器结构体指针*/ 12 #define RCC ((RCC_TypeDef *) RCC_BASE) 这些宏通过强制把外设的基地址转换成 GPIO_TypeDef 类型的地址,从而得到 GPIOA、 GPIOB 等直接指向对应外设的指针,通过结构体的指针操作,即可访问对应外设的寄存器。 利用这些指针访问寄存器,我们把 main 文件里对应的代码修改掉,见代码清单 8-3。 代码清单 8-3 使用结构体指针方式控制 LED 灯 1 /** 2 * 主函数 3 */ 4 int main(void) 5{ 6 7 RCC->AHB1ENR |= (1<<7); 8 9 /* LED 端口初始化 */ 第 58 页 共 928 零死角玩转 STM32—F429 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 } /*GPIOH MODER10 清空*/ GPIOH->MODER &= ~( 0x03<< (2*10)); /*PH10 MODER10 = 01b 输出模式*/ GPIOH->MODER |= (1<<2*10); /*GPIOH OTYPER10 清空*/ GPIOH->OTYPER &= ~(1<<1*10); /*PH10 OTYPER10 = 0b 推挽模式*/ GPIOH->OTYPER |= (0<<1*10); /*GPIOH OSPEEDR10 清空*/ GPIOH->OSPEEDR &= ~(0x03<<2*10); /*PH10 OSPEEDR10 = 0b 速率 2MHz*/ GPIOH->OSPEEDR |= (0<<2*10); /*GPIOH PUPDR10 清空*/ GPIOH->PUPDR &= ~(0x03<<2*10); /*PH10 PUPDR10 = 01b 上拉模式*/ GPIOH->PUPDR |= (1<<2*10); /*PH10 BSRR 寄存器的 BR10 置 1,使引脚输出低电平*/ GPIOH->BSRRH |= (1<<10); /*PH10 BSRR 寄存器的 BS10 置 1,使引脚输出高电平*/ //GPIOH->BSRRL |= (1<<10); while (1); 乍一看,除了最后一部分,把 BSRR 寄存器分成 BSRRH 和 BSRRL 两段,其它部分跟 直接用绝对地址访问只是名字改了而已,用起来跟上一章没什么区别。这是因为我们现在 只实现了库函数的基础,还没有定义库函数。 打好了地基,下面我们就来建高楼。接下来使用函数来封装 GPIO 的基本操作,方便 以后应用的时候不需要再查询寄存器,而是直接通过调用这里定义的函数来实现。我们把 针对 GPIO 外设操作的函数及其宏定义分别存放在“stm32f4xx_gpio.c”和 “stm32f4xx_gpio.h”文件中。 定义位操作函数 在“stm32f4xx_gpio.c”文件定义两个位操作函数,分别用于控制引脚输出高电平和低 电平,见代码清单 8-4。 代码清单 8-4 GPIO 置位函数与复位函数的定义 1 /** 2 *函数功能:设置引脚为高电平 3 *参数说明:GPIOx:该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址 4* GPIO_Pin:选择要设置的 GPIO 端口引脚,可输入宏 GPIO_Pin_0-15, 5* 表示 GPIOx 端口的 0-15 号引脚。 6 */ 7 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 8{ 9 /*设置 GPIOx 端口 BSRRL 寄存器的第 GPIO_Pin 位,使其输出高电平*/ 10 /*因为 BSRR 寄存器写 0 不影响, 11 宏 GPIO_Pin 只是对应位为 1,其它位均为 0,所以可以直接赋值*/ 12 13 GPIOx->BSRRL = GPIO_Pin; 第 59 页 共 928 零死角玩转 STM32—F429 14 } 15 16 /** 17 *函数功能:设置引脚为低电平 18 *参数说明:GPIOx:该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址 19 * GPIO_Pin:选择要设置的 GPIO 端口引脚,可输入宏 GPIO_Pin_0-15, 20 * 表示 GPIOx 端口的 0-15 号引脚。 21 */ 22 void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 23 { 24 /*设置 GPIOx 端口 BSRRH 寄存器的第 GPIO_Pin 位,使其输出低电平*/ 25 /*因为 BSRR 寄存器写 0 不影响, 26 宏 GPIO_Pin 只是对应位为 1,其它位均为 0,所以可以直接赋值*/ 27 28 GPIOx->BSRRH = GPIO_Pin; 29 } 这两个函数体内都是只有一个语句,对 GPIOx 的 BSRRL 或 BSRRH 寄存器赋值,从 而设置引脚为高电平或低电平。其中 GPIOx 是一个指针变量,通过函数的输入参数我们可 以修改它的值,如给它赋予 GPIOA、GPIOB、GPIOH 等结构体指针值,这个函数就可以 控制相应的 GPIOA、GPIOB、GPIOH 等端口的输出。 对比我们前面对 BSRR 寄存器的赋值,都是用“|=”操作来防止对其它数据位产生干 扰的,为何此函数里的操作却直接用“=”号赋值,这样不怕干扰其它数据位吗?见代码 清单 8-5。 代码清单 8-5 赋值方式对比 1 /*使用 “|=” 来赋值*/ 2 GPIOH->BSRRH |= (1<<10); 3 /*直接使用 "=" 号赋值*/ 4 GPIOx->BSRRH = GPIO_Pin; 根据 BSRR 寄存器的特性,对它的数据位写“0”,是不会影响输出的,只有对它的数 据位写“1”,才会控制引脚输出。对低 16 位写“1”输出高电平,对高 16 位写“1”输出 低电平。也就是说,假如我们对 BSRRH(高 16 位)直接用“=”操作赋二进制值“0000 0000 0000 0001 b”,它会控制 GPIO 的引脚 0 输出低电平,赋二进制值“0000 0000 0001 0000 b”,它会控制 GPIO 引脚 4 输出低电平,而其它数据位由于是 0,所以不会受到干扰。同 理,对 BSRRL(低 16 位)直接赋值也是如此,数据位为 1 的位输出高电平。代码清单 8-6 中 的两种方式赋值,功能相同。 代码清单 8-6 BSRR 寄存器赋值等效代码 1 /*使用 “|=” 来赋值*/ 2 GPIOH->BSRRH |= (uint16_t)(1<<10); 3 /*直接使用“=” 来赋值,二进制数(0000 0100 0000 0000)*/ 4 GPIOH->BSRRH = (uint16_t)(1<<10); 这两行代码功能等效,都把 BSRRH 的 bit10 设置为 1,控制引脚 10 输出低电平,且其 它引脚状态不变。但第二个语句操作效率是比较高的,因为“|=”号包含了读写操作,而 “=”号只需要一个写操作。因此在定义位操作函数中我们使用后者。 利用这两个位操作函数,就可以方便地操作各种 GPIO 的引脚电平了,控制各种端口 引脚的范例见代码清单 8-7。 第 60 页 共 928 零死角玩转 STM32—F429 代码清单 8-7 位操作函数使用范例 1 2 /*控制 GPIOH 的引脚 10 输出高电平*/ 3 GPIO_SetBits(GPIOH,(uint16_t)(1<<10)); 4 /*控制 GPIOH 的引脚 10 输出低电平*/ 5 GPIO_ResetBits(GPIOH,(uint16_t)(1<<10)); 6 7 /*控制 GPIOH 的引脚 10、引脚 11 输出高电平,使用“|”同时控制多个引脚*/ 8 GPIO_SetBits(GPIOH,(uint16_t)(1<<10)|(uint16_t)(1<<11)); 9 /*控制 GPIOH 的引脚 10、引脚 11 输出低电平*/ 10 GPIO_ResetBits(GPIOH,(uint16_t)(1<<10)|(uint16_t)(1<<10)); 11 12 /*控制 GPIOA 的引脚 8 输出高电平*/ 13 GPIO_SetBits(GPIOA,(uint16_t)(1<<8)); 14 /*控制 GPIOB 的引脚 9 输出低电平*/ 15 GPIO_ResetBits(GPIOB,(uint16_t)(1<<9)); 使用以上函数输入参数,设置引脚号时,还是稍感不便,为此我们把表示 16 个引脚的 操作数都定义成宏,见代码清单 8-8。 代码清单 8-8 选择引脚参数的宏 1 /*GPIO 引脚号定义*/ 2 #define GPIO_Pin_0 3 #define GPIO_Pin_1 4 #define GPIO_Pin_2 5 #define GPIO_Pin_3 6 #define GPIO_Pin_4 7 #define GPIO_Pin_5 8 #define GPIO_Pin_6 9 #define GPIO_Pin_7 10 #define GPIO_Pin_8 11 #define GPIO_Pin_9 12 #define GPIO_Pin_10 13 #define GPIO_Pin_11 14 #define GPIO_Pin_12 15 #define GPIO_Pin_13 16 #define GPIO_Pin_14 17 #define GPIO_Pin_15 18 #define GPIO_Pin_All (uint16_t)0x0001) /*!< 选择 Pin0 (1<<0) */ ((uint16_t)0x0002) /*!< 选择 Pin1 (1<<1)*/ ((uint16_t)0x0004) /*!< 选择 Pin2 (1<<2)*/ ((uint16_t)0x0008) /*!< 选择 Pin3 (1<<3)*/ ((uint16_t)0x0010) /*!< 选择 Pin4 */ ((uint16_t)0x0020) /*!< 选择 Pin5 */ ((uint16_t)0x0040) /*!< 选择 Pin6 */ ((uint16_t)0x0080) /*!< 选择 Pin7 */ ((uint16_t)0x0100) /*!< 选择 Pin8 */ ((uint16_t)0x0200) /*!< 选择 Pin9 */ ((uint16_t)0x0400) /*!< 选择 Pin10 */ ((uint16_t)0x0800) /*!< 选择 Pin11 */ ((uint16_t)0x1000) /*!< 选择 Pin12 */ ((uint16_t)0x2000) /*!< 选择 Pin13 */ ((uint16_t)0x4000) /*!< 选择 Pin14 */ ((uint16_t)0x8000) /*!< 选择 Pin15 */ ((uint16_t)0xFFFF) /*!< 选择全部引脚 */ 这些宏代表的参数是某位置“1”其它位置“0”的数值,其中最后一个 “GPIO_Pin_ALL”是所有数据位都为“1”,所以用它可以一次控制设置整个端口的 0-15 所有引脚。利用这些宏, GPIO 的控制代码可改为代码清单 8-9。 代码清单 8-9 使用位操作函数及宏控制 GPIO 1 2 /*控制 GPIOH 的引脚 10 输出高电平*/ 3 GPIO_SetBits(GPIOH,GPIO_Pin_10); 4 /*控制 GPIOH 的引脚 10 输出低电平*/ 5 GPIO_ResetBits(GPIOH,GPIO_Pin_10); 6 7 /*控制 GPIOH 的引脚 10、引脚 11 输出高电平,使用“|”,同时控制多个引脚*/ 8 GPIO_SetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11); 9 /*控制 GPIOH 的引脚 10、引脚 11 输出低电平*/ 10 GPIO_ResetBits(GPIOH,GPIO_Pin_10|GPIO_Pin_11); 11 /*控制 GPIOH 的所有输出低电平*/ 12 GPIO_ResetBits(GPIOH,GPIO_Pin_ALL); 13 第 61 页 共 928 零死角玩转 STM32—F429 14 /*控制 GPIOA 的引脚 8 输出高电平*/ 15 GPIO_SetBits(GPIOA,GPIO_Pin_8); 16 /*控制 GPIOB 的引脚 9 输出低电平*/ 17 GPIO_ResetBits(GPIOB,GPIO_Pin_9); 使用以上代码控制 GPIO,我们就不需要再看寄存器了,直接从函数名和输入参数就 可以直观看出这个语句要实现什么操作。(英文中―Set‖表示“置位”,即高电平,“Reset” 表示“复位”,即低电平) 8.3.3 定义初始化结构体 GPIO_InitTypeDef 定义位操作函数后,控制 GPIO 输出电平的代码得到了简化,但在控制 GPIO 输出电 平前还需要初始化 GPIO 引脚的各种模式,这部分代码涉及的寄存器有很多,我们希望初 始化 GPIO 也能以如此简单的方法去实现。为此,我们先根据 GPIO 初始化时涉及到的初 始化参数以结构体的形式封装起来,声明一个名为 GPIO_InitTypeDef 的结构体类型,见代 码清单 8-10。 代码清单 8-10 定义 GPIO 初始化结构体 1 typedef uint8_t unsigned char; 2 /** 3 * GPIO 初始化结构体类型定义 4 */ 5 typedef struct { 6 uint32_t GPIO_Pin; /*!< 选择要配置的 GPIO 引脚 7 可输入 GPIO_Pin_ 定义的宏 */ 8 9 uint8_t GPIO_Mode; /*!< 选择 GPIO 引脚的工作模式 10 可输入二进制值: 00 、01、 10、 11 11 表示输入/输出/复用/模拟 */ 12 13 uint8_t GPIO_Speed; /*!< 选择 GPIO 引脚的速率 14 可输入二进制值: 00 、01、 10、 11 15 表示 2/25/50/100MHz */ 16 17 uint8_t GPIO_OType; /*!< 选择 GPIO 引脚输出类型 18 可输入二进制值: 0 、1 19 表示推挽/开漏 */ 20 21 uint8_t GPIO_PuPd; /*!<选择 GPIO 引脚的上/下拉模式 22 可输入二进制值: 00 、01、 10 23 表示浮空/上拉/下拉*/ 24 } GPIO_InitTypeDef; 这个结构体中包含了初始化 GPIO 所需要的信息,包括引脚号、工作模式、输出速率、 输出类型以及上/下拉模式。设计这个结构体的思路是:初始化 GPIO 前,先定义一个这样 的结构体变量,根据需要配置 GPIO 的模式,对这个结构体的各个成员进行赋值,然后把 这个变量作为“GPIO 初始化函数”的输入参数,该函数能根据这个变量值中的内容去配置 寄存器,从而实现初始化 GPIO。 8.3.4 定义引脚模式的枚举类型 上面定义的结构体很直接,美中不足的是在对结构体中各个成员赋值时还需要看具体 哪个模式对应哪个数值,如 GPIO_Mode 成员的“输入/输出/复用/模拟”模式对应二进制值 第 62 页 共 928 零死角玩转 STM32—F429 “00 、01、 10、 11”,我们不希望每次用到都要去查找这些索引值,所以使用 C 语言中 的枚举语法定义这些参数,见代码清单 8-11。 代码清单 8-11 GPIO 配置参数的枚举定义 1 /** 2 * GPIO 端口配置模式的枚举定义 3 */ 4 typedef enum { 5 GPIO_Mode_IN = 0x00, /*!< 输入模式 */ 6 GPIO_Mode_OUT = 0x01, /*!< 输出模式 */ 7 GPIO_Mode_AF = 0x02, /*!< 复用模式 */ 8 GPIO_Mode_AN = 0x03 /*!< 模拟模式 */ 9 } GPIOMode_TypeDef; 10 11 /** 12 * GPIO 输出类型枚举定义 13 */ 14 typedef enum { 15 GPIO_OType_PP = 0x00, /*!< 推挽模式 */ 16 GPIO_OType_OD = 0x01 /*!< 开漏模式 */ 17 } GPIOOType_TypeDef; 18 19 /** 20 * GPIO 输出速率枚举定义 21 */ 22 typedef enum { 23 GPIO_Speed_2MHz = 0x00, /*!< 2MHz */ 24 GPIO_Speed_25MHz = 0x01, /*!< 25MHz */ 25 GPIO_Speed_50MHz = 0x02, /*!< 50MHz */ 26 GPIO_Speed_100MHz = 0x03 /*!<100MHz */ 27 } GPIOSpeed_TypeDef; 28 29 /** 30 *GPIO 上/下拉配置枚举定义 31 */ 32 typedef enum { 33 GPIO_PuPd_NOPULL = 0x00,/*浮空*/ 34 GPIO_PuPd_UP = 0x01, /*上拉*/ 35 GPIO_PuPd_DOWN = 0x02 /*下拉*/ 36 } GPIOPuPd_TypeDef; 有了这些枚举定义,我们的 GPIO_InitTypeDef 结构体也可以使用枚举类型来限定输入 了,代码清单 8-13。 代码清单 8-12 使用枚举类型定义的 GPIO_InitTypeDef 结构体成员 1 /** 2 * GPIO 初始化结构体类型定义 3 */ 4 typedef struct { 5 uint32_t GPIO_Pin; 6 7 8 GPIOMode_TypeDef GPIO_Mode; 9 10 11 GPIOSpeed_TypeDef GPIO_Speed; 12 13 14 GPIOOType_TypeDef GPIO_OType; 15 16 /*!< 选择要配置的 GPIO 引脚 可输入 GPIO_Pin_ 定义的宏 */ /*!< 选择 GPIO 引脚的工作模式 可输入 GPIOMode_TypeDef 定义的枚举值*/ /*!< 选择 GPIO 引脚的速率 可输入 GPIOSpeed_TypeDef 定义的枚举值 */ /*!< 选择 GPIO 引脚输出类型 可输入 GPIOOType_TypeDef 定义的枚举值*/ 第 63 页 共 928 零死角玩转 STM32—F429 17 GPIOPuPd_TypeDef GPIO_PuPd; 18 19 } GPIO_InitTypeDef; /*!<选择 GPIO 引脚的上/下拉模式 可输入 GPIOPuPd_TypeDef 定义的枚举值*/ 如果不使用枚举类型,仍使用“uint8_t”类型来定义结构体成员,那么成员值的范围 就是 0-255 了,而实际上这些成员都只能输入几个数值。所以使用枚举类型可以对结构体 成员起到限定输入的作用,只能输入相应已定义的枚举值。 利用这些枚举定义,给 GPIO_InitTypeDef 结构体类型赋值配置就非常直观了,范例见 代码清单 8-13。 代码清单 8-13 给 GPIO_InitTypeDef 初始化结构体赋值范例 1 GPIO_InitTypeDef InitStruct; 2 3 /* LED 端口初始化 */ 4 /*选择要控制的 GPIO 引脚*/ 5 InitStruct.GPIO_Pin = GPIO_Pin_10; 6 /*设置引脚模式为输出模式*/ 7 InitStruct.GPIO_Mode = GPIO_Mode_OUT; 8 /*设置引脚的输出类型为推挽输出*/ 9 InitStruct.GPIO_OType = GPIO_OType_PP; 10 /*设置引脚为上拉模式*/ 11 InitStruct.GPIO_PuPd = GPIO_PuPd_UP; 12 /*设置引脚速率为 2MHz */ 13 InitStruct.GPIO_Speed = GPIO_Speed_2MHz; 8.3.5 定义 GPIO 初始化函数 接着前面的思路,对初始化结构体赋值后,把它输入到 GPIO 初始化函数,由它来实 现寄存器配置。我们的 GPIO 初始化函数实现见代码清单 8-14, 代码清单 8-14 GPIO 初始化函数 1 2 /** 3 *函数功能:初始化引脚模式 4 *参数说明:GPIOx,该参数为 GPIO_TypeDef 类型的指针,指向 GPIO 端口的地址 5* GPIO_InitTypeDef:GPIO_InitTypeDef 结构体指针,指向初始化变量 6 */ 7 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) 8{ 9 uint32_t pinpos = 0x00, pos = 0x00 , currentpin = 0x00; 10 11 /*-- GPIO Mode Configuration --*/ 12 for (pinpos = 0x00; pinpos < 16; pinpos++) { 13 /*以下运算是为了通过 GPIO_InitStruct->GPIO_Pin 算出引脚号 0-15*/ 14 15 /*经过运算后 pos 的 pinpos 位为 1,其余为 0,与 GPIO_Pin_x 宏对应。 16 pinpos 变量每次循环加 1,*/ 17 pos = ((uint32_t)0x01) << pinpos; 18 19 /* pos 与 GPIO_InitStruct->GPIO_Pin 做 & 运算, 20 若运算结果 currentpin == pos, 21 则表示 GPIO_InitStruct->GPIO_Pin 的 pinpos 位也为 1, 22 从而可知 pinpos 就是 GPIO_InitStruct->GPIO_Pin 对应的引脚号:0-15*/ 23 currentpin = (GPIO_InitStruct->GPIO_Pin) & pos; 24 25 /*currentpin == pos 时执行初始化*/ 26 if (currentpin == pos) { 27 /*GPIOx 端口,MODER 寄存器的 GPIO_InitStruct->GPIO_Pin 对应的引脚, 第 64 页 共 928 零死角玩转 STM32—F429 28 MODER 位清空*/ 29 GPIOx->MODER &= ~(3 << (2 *pinpos)); 30 31 /*GPIOx 端口,MODER 寄存器的 GPIO_Pin 引脚, 32 MODER 位设置"输入/输出/复用输出/模拟"模式*/ 33 GPIOx->MODER |= (((uint32_t)GPIO_InitStruct->GPIO_Mode) << (2 *pinpos)); 34 35 /*GPIOx 端口,PUPDR 寄存器的 GPIO_Pin 引脚, 36 PUPDR 位清空*/ 37 GPIOx->PUPDR &= ~(3 << ((2 *pinpos))); 38 39 /*GPIOx 端口,PUPDR 寄存器的 GPIO_Pin 引脚, 40 PUPDR 位设置"上/下拉"模式*/ 41 GPIOx->PUPDR |= (((uint32_t)GPIO_InitStruct->GPIO_PuPd) << (2 *pinpos)); 42 43 /*若模式为"输出/复用输出"模式,则设置速度与输出类型*/ 44 if ((GPIO_InitStruct->GPIO_Mode == GPIO_Mode_OUT) || 45 (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_AF)) { 46 /*GPIOx 端口,OSPEEDR 寄存器的 GPIO_Pin 引脚, 47 OSPEEDR 位清空*/ 48 GPIOx->OSPEEDR &= ~(3 << (2 *pinpos)); 49 /*GPIOx 端口,OSPEEDR 寄存器的 GPIO_Pin 引脚, 50 OSPEEDR 位设置输出速度*/ 51 GPIOx->OSPEEDR |= ((uint32_t)(GPIO_InitStruct->GPIO_Speed)<<(2 *pinpos)); 52 53 /*GPIOx 端口,OTYPER 寄存器的 GPIO_Pin 引脚, 54 OTYPER 位清空*/ 55 GPIOx->OTYPER &= ~(1 << (pinpos)) ; 56 /*GPIOx 端口,OTYPER 位寄存器的 GPIO_Pin 引脚, 57 OTYPER 位设置"推挽/开漏"输出类型*/ 58 GPIOx->OTYPER |= (uint16_t)(( GPIO_InitStruct->GPIO_OType)<< (pinpos)); 59 } 60 } 61 } 62 } 这个函数有 GPIOx 和 GPIO_InitStruct 两个输入参数,分别是 GPIO 外设指针和 GPIO 初始化结构体指针。分别用来指定要初始化的 GPIO 端口及引脚的工作模式。 函数实现主要分两个环节: (1) 利用 for 循环,根据 GPIO_InitStruct 的结构体成员 GPIO_Pin 计算出要初始化的引 脚号。这段看起来复杂的运算实际上可以这样理解:它要通过宏“GPIO_Pin_x” 的参数计算出 x 值(宏的参数值是第 x 数据位为 1,其余为 0,参考代码清单 8-8), 计算得的引脚号结果存储在 pinpos 变量中。 (2) 得到引脚号 pinpos 后,利用初始化结构体各个成员的值,对相应寄存器进行配置, 这部分与我们前面直接配置寄存器的操作是类似的,先对引脚号 pinpos 相应的配 置位清空,后根据结构体成员对配置位赋值(GPIO_Mode 成员对应 MODER 寄存 器的配置,GPIO_PuPd 成员对应 PUPDR 寄存器的配置等)。区别是这里的寄存器 配置值及引脚号都是由变量存储的。 8.3.6 全新面貌,使用函数点亮 LED 灯 完成以上的准备后,我们就可以自己定义的函数来点亮 LED 灯了,见代码清单 8-15。 第 65 页 共 928 零死角玩转 STM32—F429 代码清单 8-15 使用函数点亮 LED 灯 1 2 /* 3 使用寄存器的方法点亮 LED 灯 4 */ 5 #include "stm32f4xx_gpio.h" 6 7 void Delay( uint32_t nCount); 8 9 /** 10 * 主函数,使用封装好的函数来控制 LED 灯 11 */ 12 int main(void) 13 { 14 GPIO_InitTypeDef InitStruct; 15 16 /*开启 GPIOH 时钟,使用外设时都要先开启它的时钟*/ 17 RCC->AHB1ENR |= (1<<7); 18 19 /* LED 端口初始化 */ 20 21 /*初始化 PH10 引脚*/ 22 /*选择要控制的 GPIO 引脚*/ 23 InitStruct.GPIO_Pin = GPIO_Pin_10; 24 /*设置引脚模式为输出模式*/ 25 InitStruct.GPIO_Mode = GPIO_Mode_OUT; 26 /*设置引脚的输出类型为推挽输出*/ 27 InitStruct.GPIO_OType = GPIO_OType_PP; 28 /*设置引脚为上拉模式*/ 29 InitStruct.GPIO_PuPd = GPIO_PuPd_UP; 30 /*设置引脚速率为 2MHz */ 31 InitStruct.GPIO_Speed = GPIO_Speed_2MHz; 32 /*调用库函数,使用上面配置的 GPIO_InitStructure 初始化 GPIO*/ 33 GPIO_Init(GPIOH, &InitStruct); 34 35 /*使引脚输出低电平,点亮 LED1*/ 36 GPIO_ResetBits(GPIOH,GPIO_Pin_10); 37 38 /*延时一段时间*/ 39 Delay(0xFFFFFF); 40 41 /*使引脚输出高电平,关闭 LED1*/ 42 GPIO_SetBits(GPIOH,GPIO_Pin_10); 43 44 /*初始化 PH11 引脚*/ 45 InitStruct.GPIO_Pin = GPIO_Pin_11; 46 GPIO_Init(GPIOH,&InitStruct); 47 48 /*使引脚输出低电平,点亮 LED2*/ 49 GPIO_ResetBits(GPIOH,GPIO_Pin_11); 50 51 while (1); 52 53 } 54 55 //简单的延时函数,让 cpu 执行无意义指令,消耗时间 56 //具体延时时间难以计算,以后我们可使用定时器精确延时 57 void Delay( uint32_t nCount) 58 { 59 for (; nCount != 0; nCount--); 60 } 61 62 // 函数为空,目的是为了骗过编译器不报错 第 66 页 共 928 零死角玩转 STM32—F429 63 void SystemInit(void) 64 { 65 } 现在看起来,使用函数来控制 LED 灯与之前直接控制寄存器已经有了很大的区别: main 函数中先定义了一个初始化结构体变量 InitStruct,然后对该变量的各个成员按点亮 LED 灯所需要的 GPIO 配置模式进行赋值,赋值后,调用 GPIO_Init 函数,让它根据结构 体成员值对 GPIO 寄存器写入控制参数,完成 GPIO 引脚初始化。控制电平时,直接使用 GPIO_SetBits 和 GPIO_Resetbits 函数控制输出。如若对其它引脚进行不同模式的初始化, 只要修改初始化结构体 InitStruct 的成员值,把新的参数值输入到 GPIO_Init 函数再调用即 可。 代码中新增的 Delay 函数,主要功能是延时,让我们可以看清楚实验现象(不延时的话 指令执行太快,肉眼看不出来),它的实现原理是让 CPU 执行无意义的指令,消耗时间, 在此不要纠结它的延时时间,写一个大概输入参数值,下载到实验板实测,觉得太久了就 把参数值改小,短了就改大即可。需要精确延时的时候我们会用 STM32 的定时器外设进行 精确延时的。 8.3.7 下载验证 把编译好的程序下载到开发板并复位,可看到板子上的灯先亮红色(LED1),后亮绿色 (LED2)。 8.3.8 总结 什么是 ST 标准软件库?这就是。 我们从寄存器映像开始,把内存跟寄存器建立起一一对应的关系,然后操作寄存器点 亮 LED,再把寄存器操作封装成一个个函数。一步一步走来,我们实现了库最简单的雏形, 如果我们不断地增加操作外设的函数,并且把所有的外设都写完,一个完整的库就实现了。 本章中的 GPIO 相关库函数及结构体定义,实际上都是从 ST 标准库搬过来的。这样分 析它纯粹是为了满足自己的求知欲,学习其编程的方式、思想,这对提高我们的编程水平 是很有好处的,顺便感受一下 ST 库设计的严谨性,我认为这样的代码不仅严谨且华丽优 美,不知您是否也有这样的感受。 与直接配置寄存器相比,从执行效率上看会有额外的消耗:初始化变量赋值的过程、 库函数在被调用的时候要耗费调用时间;在函数内部,对输入参数转换所需要的额外运算 也消耗一些时间(如 GPIO 中运算求出引脚号时)。而其它的宏、枚举等解释操作是作编译过 程完成的,这部分并不消耗内核的时间。那么函数库的优点呢?是我们可以快速上手 STM32 控制器;配置外设状态时,不需要再纠结要向寄存器写入什么数值;交流方便,查 错简单。这就是我们选择库的原因。 现在的处理器的主频是越来越高,我们不需要担心 CPU 耗费那么多时间来干活会不会 被累倒,库主要应用是在初始化过程,而初始化过程一般是芯片刚上电或在核心运算之前 的执行的,这段时间的等待是 0.02us 还是 0.01us 在很多时候并没有什么区别。相对来说, 第 67 页 共 928 零死角玩转 STM32—F429 我们还是担心一下如果都用寄存器操作,每行代码都要查《STM32F4xx 规格书》中的说明, 自己会不会被累倒吧。 在以后开发的工程中,一般不会去分析 ST 的库函数的实现了。因为外设的库函数是 很类似的,库外设都包含初始化结构体,以及特定的宏或枚举标识符,这些封装被库函数 这些转化成相应的值,写入到寄存器之中,函数内部的具体实现是十分枯燥和机械的工作。 如果您有兴趣,在您掌握了如何使用外设的库函数之后,可以查看一下它的源码实现。 通常我们只需要通过了解每种外设的“初始化结构体”就能够通过它去了解 STM32 的 外设功能及控制了。 8.4 每课一问 1. 阅读《STM32F4xx 参考手册》及《STM32F4xx 规格书》中关于 USART 外设(通用同 步异步收发器)的寄存器说明及地址映射,参考“GPIO_TypeDef”的结构体声明,封 装 USART 的寄存器成一个 USART_TypeDef 类型,并定义 USART1、USART2、 USART3 外设的结构体访问指针。 2. 参考 GPIO_SetBits 的函数实现,定义一个能读取 GPIO 引脚状态的函数,函数声明: “uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)”,要 求能够返回输入参数 GPIOx 端口的 GPIO_Pin 引脚的电平状态,高电平时返回 1,低 电平返回 0。 第 68 页 共 928 零死角玩转 STM32—F429 第9章 初识 STM32 固件库 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、《Cortex-M3 权 威指南》, STM32 标准库帮助文档:《stm32f4xx_dsp_stdperiph_lib_um.chm》。 在上一章中,我们构建了几个控制 GPIO 外设的函数,算是实现了函数库的雏形,但 GPIO 还有很多功能函数我们没有实现,而且 STM32 芯片不仅仅只有 GPIO 这一个外设。 如果我们想要亲自完成这个函数库,工作量是非常巨大的。ST 公司提供的标准软件库,包 含了 STM32 芯片所有寄存器的控制操作,我们直接学习如何使用 ST 标准库,会极大地方 便控制 STM32 芯片。 9.1 CMSIS 标准及库层次关系 因为基于 Cortex 系列芯片采用的内核都是相同的,区别主要为核外的片上外设的差异, 这些差异却导致软件在同内核,不同外设的芯片上移植困难。为了解决不同的芯片厂商生 产的 Cortex 微控制器软件 的兼容性问题,ARM 与芯片厂商建立了 CMSIS 标准(Cortex MicroController Software Interface Standard)。 所谓 CMSIS 标准,实际是新建了一个软件抽象层。见图 9-1。 用 用户代码 户 层 CMSIS-DSP DSP 库 CMSIS 实时系统 API CMSIS 层 CMSIS 核心层 SIMD Cortex-M4 实时系统内核 设备外设函数(如 STM32 驱动函数库) 核内外设函数 外设寄存器定义&中断向量定义 MCU Cortex 内 核 SysTick 实时系统 内核时钟 NVIC 中断 控制器 调试跟踪 模块 其它外设 图 9-1 CMSIS 架构 CMSIS 标准中最主要的为 CMSIS 核心层,它包括了: 第 69 页 共 928 零死角玩转 STM32—F429  内核函数层:其中包含用于访问内核寄存器的名称、地址定义,主要由 ARM 公司提 供。  设备外设访问层:提供了片上的核外外设的地址和中断定义,主要由芯片生产商提供。 可见 CMSIS 层位于硬件层与操作系统或用户层之间,提供了与芯片生产商无关的硬件 抽象层,可以为接口外设、实时操作系统提供简单的处理器软件接口,屏蔽了硬件差异, 这对软件的移植是有极大的好处的。STM32 的库,就是按照 CMSIS 标准建立的。 9.1.1 库目录、文件简介 STM32 标准库可以从官网获得,也可以直接从本书的配套资料得到。本书讲解的例程 全部采用 1.5.1 库文件。以下内容请大家打开 STM32 标准库文件配合阅读。 解压库文件后进入其目录: “STM32F4xx_DSP_StdPeriph_Lib_V1.5.1\” 软件库各文件夹的内容说明见图 9-2。 图 9-2 ST 标准库 目录:STM32F4xx_DSP_StdPeriph_Lib_V1.5.1\  Libraries:文件夹下是驱动库的源代码及启动文件。  Project :文件夹下是用驱动库写的例子和工程模板。  Utilities:包含了基于 ST 官方实验板的例程,以及第三方软件库,如 emwin 图形软件 库、fatfs 文件系统。  MCD-ST Liberty…:库文件的 License 说明。  Release_Note.html::库的版本更新说明。  stm32f4xx_dsp_stdperiph…: 库帮助文档,这是一个已经编译好的 HTML 文件,主要 讲述如何使用驱动库来编写自己的应用程序。说得形象一点,这个 HTML 就是告诉我 们:ST 公司已经为你写好了每个外设的驱动了,想知道如何运用这些例子就来向我求 救吧。不幸的是,这个帮助文档是英文的,这对很多英文不好的朋友来说是一个很大 的障碍。但这里要告诉大家,英文仅仅是一种工具,绝对不能让它成为我们学习的障 碍。其实这些英文还是很简单的,我们需要的是拿下它的勇气。 第 70 页 共 928 零死角玩转 STM32—F429 在使用库开发时,我们需要把 libraries 目录下的库函数文件添加到工程中,并查阅库 帮助文档来了解 ST 提供的库函数,这个文档说明了每一个库函数的使用方法。 进入 Libraries 文件夹看到,关于内核与外设的库文件分别存放在 CMSIS 和 STM32F4xx_StdPeriph_Driver 文件夹中。 先看看 CMSIS 文件夹。 STM32F4xx_DSP_StdPeriph_Lib_V1.5.1\Libraries\CMSIS\文件夹下内容见图 9-3。 图 9-3 CMSIS 文件夹内容 目录:Libraries\CMSIS\ 其中 Device 与 Include 中的文件是我们使用得最多的,先讲解这两个文件夹中的内容。 1. Include 文件夹 在 Include 文件夹中包含了 的是位于 CMSIS 标准的核内设备函数层的 Cortex-M 核通用 的头文件,它们的作用是为那些采用 Cortex-M 核设计 SOC 的芯片商设计的芯片外设提供 一个进入内核的接口,定义了一些内核相关的寄存器(类似我们前面写的 stm32f4xx.h 文件, 但定义的是内核部分的寄存器)。这些文件在其它公司的 Cortex-M 系列芯片也是相同的。 至于这些功能是怎样用源码实现的,可以不用管它,只需把这些文件加进我们的工程文件 即可,有兴趣的朋友可以深究,关于内核的寄存器说明,需要查阅《cortex_m4_Technical Reference Manual》及《Cortex™-M4 内核参考手册》文档,《STM32 参考手册》只包含片 上外设说明,不包含内核寄存器。 我们写 STM32F4 的工程,必须用到其中的四个文件:core_cm4.h、core_cmFunc.h、 corecmInstr.h、core_cmSimd.h,其它的文件是属于其它内核的,还有几个文件是 DSP 函数 库使用的头文件。 core_cm4.c 文件有一些与编译器相关条件编译语句,用于屏蔽不同编译器的差异。里 面包含了一些跟编译器相关的信息,如:“__CC_ARM ”(本书采用的 RVMDK、KEIL), “__GNUC__ ”(GNU 编译器)、“ICC Compiler” (IAR 编译器)。这些不同的编译器对于 C 嵌入汇编或内联函数关键字的语法不一样,这段代码统一使用“__ASM、__INLINE”宏 来定义,而在不同的编译器下,宏自动更改到相应的值,实现了差异屏蔽,见代码清单 9-1。 第 71 页 共 928 零死角玩转 STM32—F429 代码清单 9-1:core_cm3.c 文件中对编译器差异的屏蔽 使用 RVMDK 编译器时 1 #if defined ( __CC_ARM ) 的嵌入汇编与内联函数 2 #define __ASM __asm /*!< asm key的wo关rd键f字o形r 式ARM Compiler */ 3 #define __INLINE __inline /*!< inline keyword for ARM Compiler*/ 4 #define __STATIC_INLINE static __inline 5 6 #elif defined ( __GNUC__ ) 7 #define __ASM __asm /*!< asm keyword for GNU Compiler */ 8 #define __INLINE inline /*!< inline keyword for GNU Compiler */ 9 #define __STATIC_INLINE static inline 10 11 #elif defined ( __ICCARM__ ) 使用 GNU 编译器时的 形式 12 #define __ASM __asm /*!< asm keyword for IAR Compiler */ 13 /*!< inline keyword for IAR Compiler. */ 14 #define __STATIC_INLINE static inline 15 #define __INLINE inline 16 17 #elif defined ( __TMS470__ ) 18 #define __ASM __asm /*!< asm keyword for TI CCS Compiler */ 19 #define __STATIC_INLINE static inline 20 21 #elif defined ( __TASKING__ ) 22 #define __ASM __asm /*!< asm keyword for TASKING Compiler */ 23 #define __INLINE inline /*!< inline keyword for TASKING Compiler */ 24 #define __STATIC_INLINE static inline 25 26 #elif defined ( __CSMC__ ) 27 #define __packed 28 #define __ASM _asm /*!< asm keyword for COSMIC Compiler */ 29 /*use -pc99 on compile line !< inline keyword for COSMIC Compiler */ 30 #define __INLINE inline 31 #define __STATIC_INLINE static inline 32 33 #endif 较重要的是在 core_cm4.c 文件中包含了“stdint.h” 这个头文件,这是一个 ANSI C 文 件,是独立于处理器之外的,就像我们熟知的 C 语言头文件 “stdio.h” 文件一样。位于 RVMDK 这个软件的安装目录下,主要作用是提供一些类型定义。见代码清单 9-2。 代码清单 9-2:stdint.c 文件中的类型定义 1. /* exact-width signed integer types */ 2. typedef signed char int8_t; 3. typedef signed short int int16_t; 4. typedef signed int int32_t; 5. typedef signed __int64 int64_t; 6. 7. /* exact-width unsigned integer types */ 8. typedef unsigned char uint8_t; 9. typedef unsigned short int uint16_t; 10. typedef unsigned int uint32_t; 11. typedef unsigned __int64 uint64_t; 这些新类型定义屏蔽了在不同芯片平台时,出现的诸如 int 的大小是 16 位,还是 32 位 的差异。所以在我们以后的程序中,都将使用新类型如 uint8_t 、uint16_t 等。 第 72 页 共 928 零死角玩转 STM32—F429 在稍旧版的程序中还经常会出现如 u8、u16、u32 这样的类型,分别表示的无符号的 8 位、16 位、32 位整型。初学者碰到这样的旧类型感觉一头雾水,它们定义的位置在 STM32f4xx.h 文件中。建议在以后的新程序中尽量使用 uint8_t 、uint16_t 类型的定义。 core_cm4.c 跟启动文件一样都是底层文件,都是由 ARM 公司提供的,遵守 CMSIS 标 准,即所有 CM4 芯片的库都带有这个文件,这样软件在不同的 CM4 芯片的移植工作就得 以简化。 2. Device 文件夹 在 Device 文件夹下的是具体芯片直接相关的文件,包含启动文件、芯片外设寄存器定 义、系统时钟初始化功能的一些文件,这是由 ST 公司提供的。  system_stm32f4xx.c 文件 文件目录:\Libraries\CMSIS\Device\ST\STM32F4xx\Source\Templates 这个文件包含了 STM32 芯片上电后初始化系统时钟、扩展外部存储器用的函数,例如 我们前两章提到供启动文件调用的“SystemInit”函数,用于上电后初始化时钟,该函数的 定义就存储在 system_stm32f4xx.c 文件。STM32F429 系列的芯片,调用库的这个 SystemInit 函数后,系统时钟被初始化为 180MHz,如有需要可以修改这个文件的内容,设 置成自己所需的时钟频率。  启动文件 文件目录:Libraries\CMSIS\Device\ST\STM32F4xx\Source\Templates 在这个目录下,还有很多文件夹,如“ARM”、“gcc_ride7”、“iar”等,这些文件 夹下包含了对应编译平台的汇编启动文件,在实际使用时要根据编译平台来选择。我们使 用的 MDK 启动文件在“ARM”文件夹中。其中的“strartup_stm32f429_439xx.s”即为 STM32F429 芯片的启动文件,前面两章工程中使用的启动文件就是从这里复制过去的。如 果使用其它型号的芯片,要在此处选择对应的启动文件,如 STM32F446 型号使用 “startup_stm32f446xx.s”文件。  stm32f4xx.h 文件 文件目录: Libraries\CMSIS\Device\ST\STM32F4xx\Include stm32f4xx.h 这个文件非常重要,是一个 STM32 芯片底层相关的文件。它是我们前两 章自己定义的“stm32f4xx.h”文件的完整版,包含了 STM32 中所有的外设寄存器地址和结 构体类型定义,在使用到 STM32 标准库的地方都要包含这个头文件。 CMSIS 文件夹中的主要内容就是这样,接下来我们看看 STM32F4xx_StdPeriph_Driver 文件夹。 3. STM32F10x_StdPeriph_Driver 文件夹 文件目录:Libraries\STM32F4xx_StdPeriph_Driver 进入 libraries 目录下的 STM32F4xx_StdPeriph_Driver 文件夹,见图 9-4。 第 73 页 共 928 零死角玩转 STM32—F429 图 9-4 外设驱动 STM32F4xx_StdPeriph_Driver 文件夹下有 inc(include 的缩写)跟 src(source 的简写) 这两个文件夹,这里的文件属于 CMSIS 之外的的、芯片片上外设部分。src 里面是每个设 备外设的驱动源程序,inc 则是相对应的外设头文件。src 及 inc 文件夹是 ST 标准库的主要 内容,甚至不少人直接认为 ST 标准库就是指这些文件,可见其重要性。 在 src 和 inc 文件夹里的就是 ST 公司针对每个 STM32 外设而编写的库函数文件,每个 外设对应一个 .c 和 .h 后缀的文件。我们把这类外设文件统称为:stm32f4xx_ppp.c 或 stm32f4xx_ppp.h 文件,PPP 表示外设名称。如在上一章中我们自建的 stm32f4xx_gpio.c 及 stm32f4xx_gpio.h 文件,就属于这一类。 如针对模数转换(ADC)外设,在 src 文件夹下有一个 stm32f4xx_adc.c 源文件,在 inc 文 件夹下有一个 stm32f4xx_adc.h 头文件,若我们开发的工程中用到了 STM32 内部的 ADC, 则至少要把这两个文件包含到工程里。见图 9-5。 图 9-5 驱动的源文件及头文件 第 74 页 共 928 零死角玩转 STM32—F429 这两个文件夹中,还有一个很特别的 misc.c 文件,这个文件提供了外设对内核中的 NVIC(中断向量控制器)的访问函数,在配置中断时,我们必须把这个文件添加到工程中。 4. stm32f4xx_it.c、 stm32f4xx_conf.h 文件 文件目录: STM32F4xx_DSP_StdPeriph_Lib_V1.5.1\Project\STM32F4xx_StdPeriph_Templates 在这个文件目录下,存放了官方的一个库工程模板,我们在用库建立一个完整的工程 时,还需要添加这个目录下的 stm32f4xx_it.c、stm32f4xx_it.h、stm32f4xx_conf.h 这三个文 件。 stm32f4xx_it.c:这个文件是专门用来编写中断服务函数的,在我们修改前,这个文件 已经定义了一些系统异常(特殊中断)的接口,其它普通中断服务函数由我们自己添加。但 是我们怎么知道这些中断服务函数的接口如何写?是不是可以自定义呢?答案当然不是的, 这些都有可以在汇编启动文件中找到,在学习中断和启动文件的时候我们会详细介绍 stm32f4xx_conf.h:这个文件被包含进 stm32f4xx.h 文件。ST 标准库支持所有 STM32F4 型号的芯片,但有的型号芯片外设功能比较多,所以使用这个配置文件根据芯片 型号增减 ST 库的外设文件。见代码清单 9-3,针对 STM32F429 和 STM32F427 型号芯片的 差异,它们实际包含不一样的头文件,我们通过宏来指定芯片的型号。 代码清单 9-3 stm32f4xx_conf.h 文件配置软件库 1 2 #if defined (STM32F429_439xx) || defined(STM32F446xx) 3 #include "stm32f4xx_cryp.h" 4 #include "stm32f4xx_hash.h" 5 #include "stm32f4xx_rng.h" 6 #include "stm32f4xx_can.h" 7 #include "stm32f4xx_dac.h" 8 #include "stm32f4xx_dcmi.h" 9 #include "stm32f4xx_dma2d.h" 10 #include "stm32f4xx_fmc.h" 11 #include "stm32f4xx_ltdc.h" 12 #include "stm32f4xx_sai.h" 13 #endif /* STM32F429_439xx || STM32F446xx */ 14 15 #if defined (STM32F427_437xx) 16 #include "stm32f4xx_cryp.h" 17 #include "stm32f4xx_hash.h" 18 #include "stm32f4xx_rng.h" 19 #include "stm32f4xx_can.h" 20 #include "stm32f4xx_dac.h" 21 #include "stm32f4xx_dcmi.h" 22 #include "stm32f4xx_dma2d.h" 23 #include "stm32f4xx_fmc.h" 24 #include "stm32f4xx_sai.h" 25 #endif /* STM32F427_437xx */ 26 27 #if defined (STM32F40_41xxx) stm32f4xx_conf.h 这个文件还可配置是否使用“断言”编译选项,见代码清单 9-4。 代码清单 9-4 断言配置 1 #ifdef USE_FULL_ASSERT 2 3 /** 第 75 页 共 928 零死角玩转 STM32—F429 4 * @brief The assert_param macro is used for parameters check. 5 * @param expr: If expr is false, it calls assert_failed function 6 * which reports the name of the source file and the source 7 * line number of the call that failed. 8 * If expr is true, it returns no value. 9 * @retval None 10 */ 11 #define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__)) 12 /* Exported functions ---------------------------------- */ 13 void assert_failed(uint8_t* file, uint32_t line); 14 #else 15 #define assert_param(expr) ((void)0) 16 #endif /* USE_FULL_ASSERT */ 在 ST 标准库的函数中,一般会包含输入参数检查,即上述代码中的“assert_param” 宏,当参数不符合要求时,会调用“assert_failed”函数,这个函数默认是空的。 实际开发中使用断言时,先通过定义 USE_FULL_ASSERT 宏来使能断言,然后定义 “assert_failed”函数,通常我们会让它调用 printf 函数输出错误说明。使能断言后,程序 运行时会检查函数的输入参数,当软件经过测试,可发布时,会取消 USE_FULL_ASSERT 宏来去掉断言功能,使程序全速运行。 9.1.2 库各文件间的关系 前面向大家简单介绍了各个库文件的作用,库文件是直接包含进工程即可,丝毫不用 修改,而有的文件就要我们在使用的时候根据具体的需要进行配置。接下来从整体上把握 一下各个文件在库工程中的层次或关系,这些文件对应到 CMSIS 标准架构上。见图 9-6。 第 76 页 共 928 零死角玩转 STM32—F429 用 用户需要配置的文件 户 层 stm32f4xx_it.c 中断服务函数 Application.c 用户自定义的应用程 序文件 stm32f4xx_it.h 中断服务函数头文件 被 调 用 stm32f4xx_conf.h 软件库配置头文件 设备外设函数文件(STM32 外设驱动函数) 外设驱动源文件 配 misc.c stm32f4xx_ppp.c 置 使 用 什 么 外 包 含 于 设 外设驱动头文件 层 misc.h stm32f4xx_ppp.h CMSIS STM32 库文件结构 CMSIS 内核相关文件 system_stm32f4xx.c system.stm32f4xx.h core_cm4.c core_cm4.h 核 心 层 外设寄存器定义 stm32f4xx.h 定义寄存器地址、寄存器数据结构 MCU Cortex 内 SysTick 核 实时系统 NVIC 中 调试跟踪 其它外设 层 内核时钟 断控制器 模块 图 9-6 库各文件关系 图 9-6 描述了 STM32 库各文件之间的调用关系,这个图省略了 DSP 核和实时系统层 部分的文件关系。在实际的使用库开发工程的过程中,我们把位于 CMSIS 层的文件包含进 工程,除了特殊系统时钟需要修改 system_stm32f4xx.c,其它文件丝毫不用修改,也不建议 修改。 第 77 页 共 928 零死角玩转 STM32—F429 对于位于用户层的几个文件,就是我们在使用库的时候,针对不同的应用对库文件进 行增删(用条件编译的方法增删)和改动的文件。 9.2 使帮助文档 我坚信,授之以鱼不如授之以渔。官方资料是所有关于 STM32 知识的源头,所以在本 小节介绍如何使用官方资料。官方的帮助手册,是最好的教程,几乎包含了所有在开发过 程中遇到的问题。这些资料已整理到了本书附录资料中。 9.2.1 常用官方资料  《STM32F4xx 参考手册》 这个文件全方位介绍了 STM32 芯片的各种片上外设,它把 STM32 的时钟、存储器架 构、及各种外设、寄存器都描述得清清楚楚。当我们对 STM32 的外设感到困惑时,可查阅 这个文档。以直接配置寄存器方式开发的话,查阅这个文档寄存器部分的频率会相当高, 但这样效率太低了。  《STM32F4xx 规格书》 本文档相当于 STM32 的 datasheet,包含了 STM32 芯片所有的引脚功能说明及存储器 架构、芯片外设架构说明。后面我们使用 STM32 其它外设时,常常需要查找这个手册,了 解外设对应到 STM32 的哪个 GPIO 引脚。  《Cortex™-M4 内核参考手册》 本文档由 ST 公司提供,主要讲解 STM32 内核寄存器相关的说明,例如系统定时器、 中断等寄存器。这部分的内容是《STM32F4xx 参考手册》没涉及到的内核部分的补充。相 对来说,本文档虽然介绍了内核寄存器,但不如以下两个文档详细,要了解内核时,可作 为以下两个手册的配合资料使用。  《Cortex-M3 权威指南》、《cortex_m4_Technical Reference Manual》。 这两个手册是由 ARM 公司提供的,它详细讲解了 Cortex 内核的架构和特性,要深入 了解 Cortex-M 内核,这是首选,经典中的经典,其中 Cortex-M3 版本有中文版,方便学习。 因为 Cortex-M4 内核与 Cortex-M3 内核大部分相同,可用它来学习,而 Cortex-M4 新增的 特性,则必须参考《cortex_m4_Technical Reference Manual》文档了,目前只有英文版。  《stm32f4xx_dsp_stdperiph_lib_um.chm》 这个就是本章提到的库的帮助文档,在使用库函数时,我们最好通过查阅此文件来了 解标准库提供了哪些外设、函数原型或库函数的调用的方法。也可以直接阅读源码里面的 函数的函数说明。 第 78 页 共 928 零死角玩转 STM32—F429 9.2.2 初识库函数 所谓库函数,就是 STM32 的库文件中为我们编写好的函数接口,我们只要调用这些库 函数,就可以对 STM32 进行配置,达到控制目的。我们可以不知道库函数是如何实现的, 但我们调用函数必须要知道函数的功能、可传入的参数及其意义、和函数的返回值。 于是,有读者就问那么多函数我怎么记呀?我的回答是:会查就行了,哪个人记得了 那么多。所以我们学会查阅库帮助文档 是很有必要的。 打开库帮助文档《stm32f4xx_dsp_stdperiph_lib_um.chm》见图 9-7 图 9-7 库帮助文档 层层打开文档的目录标签: 标签目录:Modules\STM32F4xx_StdPeriph_Driver\ 可看到 STM32F4xx _StdPeriph_Driver 标签下有很多外设驱动文件的名字 MISC、ADC、 BKP、CAN 等标签。 我们试着查看 GPIO 的“位设置函数 GPIO_SetBits”看看,打开标签: 标签目录:Modules\STM32F4xx_StdPeriph_Driver\GPIO\Functions\GPIO_SetBits 见图 9-8。 第 79 页 共 928 零死角玩转 STM32—F429 图 9-8 库帮助文档的函数说明 利用这个文档,我们即使没有去看它的具体源代码,也知道要怎么利用它了。 如 GPIO_SetBits,函数的原型为 void GPIO_SetBits(GPIO_TypeDef * GPIOx , uint16_t GPIO_Pin)。它的功能是:输入一个类型为 GPIO_TypeDef 的指针 GPIOx 参数,选定要控 制的 GPIO 端口;输入 GPIO_Pin_x 宏,其中 x 指端口的引脚号,指定要控制的引脚。 其中输入的参数 GPIOx 为 ST 标准库中定义的自定义数据类型,这两个传入参数均为 结构体指针。初学时,我们并不知道如 GPIO_TypeDef 这样的类型是什么意思,可以点击 函数原型中带下划线的 GPIO_TypeDef 就可以查看这个类型的声明了。 就这样初步了解了一下库函数,读者就可以发现 STM32 的库是写得很优美的。每个函 数和数据类型都符合见名知义的原则,当然,这样的名称写起来特别长,而且对于我们来 说要输入这么长的英文,很容易出错,所以在开发软件的时候,在用到库函数的地方,直 接把库帮助文档中的函数名称复制粘贴到工程文件就可以了。而且,配合 MDK 软件的代 码自动补全功能,可以减少输入量。 有的用户觉得使用库文档麻烦,也可以直接查阅 STM32 标准库的源码,库帮助文档的 说明都是根据源码生成的,所以直接看源码也可以了解函数功能。 9.3 每课一问 1. 打开 ST 标准库,查看它的各个文件,对比一下 ST 标准库与我们上一章中工程里的同 名文件,查看有何差异。 第 80 页 共 928 零死角玩转 STM32—F429 第10章 新建工程—库函数版 了解 STM32 的标准库文件之后,我们就可以使用它来建立工程了,因为用库新建工程 的步骤较多,我们一般是使用库建立一个空的工程,作为工程模板。以后直接复制一份工 程模板,在它之进行开发。 本章的“工程模板”范例可在配套资料中找到,自己新建工程模版时可参考该工程。 10.1 新建工程 版本说明:MDK5.15 (MDK 即 KEIL 软件) 版本号可从 MDK 软件的“Help-->About uVision”选项中查询到。 10.1.1 新建本地工程文件夹 为了工程目录更加清晰,我们在本地电脑上新建一个“工程模板”文件夹,在它之下 再新建 6 个文件夹,具体如下: 表 10-1 工程目录文件夹清单 名称 Doc Libraries Listing Output Project User 作用 用来存放程序说明的文件,由写程序的人添加 存放的是库文件 存放编译器编译时候产生的 C/汇编/链接的列表清单 存放编译产生的调试信息、hex 文件、预览信息、封装库等 用来存放工程 用户编写的驱动文件 图 10-1 工程文件夹目录 在本地新建好文件夹后,把准备好的库文件添加到相应的文件夹下: 表 10-2 工程目录文件夹内容清单 名称 Doc Libraries Listing Output Project 作用 工程说明.txt CMSIS:里面放着跟 CM4 内核有关的库文件 STM32F4xx_StdPeriph_Driver:STM32 外设库文件 暂时为空 暂时为空 暂时为空 第 81 页 共 928 零死角玩转 STM32—F429 User stm32f4xx_conf.h:用来配置库的头文件 stm32f4xx_it.h stm32f4xx_it.c:中断相关的函数都在这个文件编写,暂时为空 main.c:main 函数文件 10.1.2 新建工程 打开 KEIL5,新建一个工程,工程名根据喜好命名,我这里取 LED-LIB,保存在 Project\RVMDK(uv5)文件夹下。 图 10-2 在 KEIL5 中新建工程 1. 选择 CPU 型号 这个根据你开发板使用的 CPU 具体的型号来选择, M4 至尊版选 STM32F429IGT 型号。 如果这里没有出现你想要的 CPU 型号,或者一个型号都没有,那么肯定是你的 KEIL5 没 有添加 device 库,KEIL5 不像 KEIL4 那样自带了很多 MCU 的型号,KEIL5 需要自己添加, 关于如何添加请参考《如何安装 KEIL5》这一章。 图 10-3 选择具体的 CPU 型号 第 82 页 共 928 零死角玩转 STM32—F429 2. 在线添加库文件 等下我们手动添加库文件,这里我们点击关掉。 图 10-4 库文件管理 3. 添加组文件夹 在新建的工程中添加 5 个组文件夹,用来存放各种不同的文件,文件从本地建好的工 程文件夹下获取,双击组文件夹就会出现添加文件的路径,然后选择文件即可。 表 10-3 工程内组文件夹内容清单 名称 STARTUP STM32F4xx _StdPeriph_Drive r USER DOC 作用 存放汇编的启动文件:startup_stm32f429_439xx.s 与 STM32 外设相关的库文件 misc.c stm32f4xx_ppp.c(ppp 代表外设名称) 用户编写的文件: main.c:main 函数文件,暂时为空 stm32f4xx_it.c:跟中断有关的函数都放这个文件,暂时为空 工程说明.txt:程序说明文件,用于说明程序的功能和注意事项等 第 83 页 共 928 零死角玩转 STM32—F429 图 10-5 如何在工程中添加文件夹 4. 添加文件 先把上面提到的文件从 ST 标准库中复制到工程模版对应文件夹的目录下,然后在新 建的工程中添加这些文件,双击组文件夹就会出现添加文件的路径,然后选择文件即可。 图 10-6 如何在工程中添加文件 5. 设置文件是否加入编译 STM32F429 比较特殊,它有用 FMC 外设代替了 FSMC 外设的功能,所以它的库文件 与其它型号的芯片不一样,在添加外设文件时,stm32f4xx_fmc.c 和 stm32f4xx_fsmc.c 文件 只能存在一个,而且我们的 STM32F429 芯片必须用 fmc 文件。如果我们把外设库的所有 第 84 页 共 928 零死角玩转 STM32—F429 文件都添加进工程,也可以使用下面的方法,设置文件不加入编译,这样也不会导致编译 问题。这种设置在开发时也很常用,暂时不把文件加进编译,方便调试。 图 10-7 设置文件是否加入编译 6. 配置魔术棒选项卡 这一步的配置工作很重要,很多人串口用不了 printf 函数,编译有问题,下载有问题, 都是这个步骤的配置出了错。 (1) Target 中选中微库“ Use MicroLib”,为的是在日后编写串口驱动的时候可以使用 printf 函数。而且有些应用中如果用了 STM32 的浮点运算单元 FPU,一定要同时开微 库,不然有时会出现各种奇怪的现象。FPU 的开关选项在微库配置选项下方的“Use Single Precision”中,默认是开的。 第 85 页 共 928 零死角玩转 STM32—F429 图 10-8 添加微库 (2) 在 Output 选项卡中把输出文件夹定位到我们工程目录下的“output”文件夹,如果想 在编译的过程中生成 hex 文件,那么那 Create HEX File 选项勾上。 图 10-9 配置 Output 选项卡 (3) 在 Listing 选项卡中把输出文件夹定位到我们工程目录下的“Listing”文件夹。 第 86 页 共 928 零死角玩转 STM32—F429 图 10-10 配置 Listing 选项卡 (4) 在 C/C++选项卡中添加处理宏及编译器编译的时候查找的头文件路径。 第 87 页 共 928 零死角玩转 STM32—F429 图 10-11 配置 C/C++ 选项卡 在这个选项中添加宏,就相当于我们在文件中使用“#define”语句定义宏一样。在编 译器中添加宏的好处就是,只要用了这个模版,就不用源文件中修改代码。  STM32F429_439xx 宏:为了告诉 STM32 标准库,我们使用的芯片是 STM32F429 型号,使 STM32 标准库根据我们选定的芯片型号来配置。  USE_STDPERIPH_DRIVER 宏:为了让 stm32f4xx.h 包含 stm32f4xx_conf.h 这个头 文件。 第 88 页 共 928 零死角玩转 STM32—F429 “Include Paths ”这里添加的是头文件的路径,如果编译的时候提示说找不到头文件, 一般就是这里配置出了问题。你把头文件放到了哪个文件夹,就把该文件夹添加到这里即 可。(请使用图中的方法用文件浏览器去添加路径,不要直接手打路径,容易出错) 7. 下载器配置 这部分的配置最好是在安装好下载器驱动,下载器连接了电脑和开发板,且开发板上 电后来配置。 这里面需要根据你使用了什么仿真器来配置,常用的有三种仿真器:JLINK/ARM-OB, ST-LINK,ULINK2,而且这个配置不是配置完一次之后以后就不会改变,当你换了芯片型 号,或者其他操作(具体原因不明)都会改变下载器的配置。 (1) JLINK/ARM-OB 配置 要先安装了 JLINK 驱动之后,该配置才能下载,两者缺一不可。 ②ST-LINK 配置 图 10-12 JLINK/ARM-OB 下载配置 第 89 页 共 928 零死角玩转 STM32—F429 要先安装了 ST-LINK 驱动之后,该配置才能下载,两者缺一不可。 图 10-13ST-LINK 下载配置 ③ULINK2 配置 要先安装了 ULINK2 驱动之后,该配置才能下载,两者缺一不可。要注意的是设置成 ULINK2,而不是 ULINK。 第 90 页 共 928 零死角玩转 STM32—F429 图 10-14ULINK2 下载配置 8. 选择 CPU 型号 这一步的配置也不是配置一次之后完事,常常会因为各种原因需要重新选择,当你下 载的时候,提示说找不到 Device 的时候,请确保该配置是否正确。有时候下载程序之后, 不会自动运行,要手动复位的时候,也回来看看这里的“Reset and Run”配置是否失效。 M4 至尊版用的 STM32 的 FLASH 大小是 1M,所以这时选择 1M 的容量,如果使用的是其 他型号的,要根据实际情况选择。 第 91 页 共 928 零死角玩转 STM32—F429 图 10-15 选择芯片型号 一个新的工程模版新建完毕。 10.2 每课一问 1. 参考本章配套的“工程模版”范例,建立一个自己的工程模板,确保编译后没有警告 及错误。 第 92 页 共 928 第11章 零死角玩转 STM32—F429 GPIO 输出—使用固件库点亮 LED 本章参考资料:《STM32F4xx 参考手册》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 利用库建立好的工程模板,就可以方便地使用 STM32 标准库编写应用程序了,可以说 从这一章我们才开始迈入 STM32 开发的大门。 LED 灯的控制使用到 GPIO 外设的基本输出功能,本章中不再赘述 GPIO 外设的概念, 如您忘记了,可重读前面“GPIO 框图剖析”小节,STM32 标准库中 GPIO 初始化结构体 GPIO_TypeDef 的定义与“定义引脚模式的枚举类型”小节中讲解的相同。 11.1 硬件设计 本实验板连接了一个 RGB 彩灯及一个普通 LED 灯,RGB 彩灯实际上由三盏分别为红 色、绿色、蓝色的 LED 灯组成,通过控制 RGB 颜色强度的组合,可以混合出各种色彩。 图 11-1 LED 硬件原理图 这些 LED 灯的阴极都是连接到 STM32 的 GPIO 引脚,只要我们控制 GPIO 引脚的电平 输出状态,即可控制 LED 灯的亮灭。图中左上方,其中彩灯的阳极连接到的一个电路图符 号“口口”,它表示引出排针,即此处本身断开,须通过跳线帽连接排针,把电源跟彩灯 的阳极连起来,实验时需注意。 若您使用的实验板 LED 灯的连接方式或引脚不一样,只需根据我们的工程修改引脚即 可,程序的控制原理相同。 11.2 软件设计 这里只讲解核心部分的代码,有些变量的设置,头文件的包含等可能不会涉及到,完 整的代码请参考本章配套的工程。 为了使工程更加有条理,我们把 LED 灯控制相关的代码独立分开存储,方便以后移植。 在“工程模板”之上新建“bsp_led.c”及“bsp_led.h”文件,其中的“bsp”即 Board Support Packet 的缩写(板级支持包),这些文件也可根据您的喜好命名,这些文件不属于 STM32 标准库的内容,是由我们自己根据应用需要编写的。 第 93 页 共 928 零死角玩转 STM32—F429 11.2.1 编程要点 1. 使能 GPIO 端口时钟; 2. 初始化 GPIO 目标引脚为推挽输出模式; 3. 编写简单测试程序,控制 GPIO 引脚输出高、低电平。 11.2.2 代码分析 1. LED 灯引脚宏定义 在编写应用程序的过程中,要考虑更改硬件环境的情况,例如 LED 灯的控制引脚与当 前的不一样,我们希望程序只需要做最小的修改即可在新的环境正常运行。这个时候一般 把硬件相关的部分使用宏来封装,若更改了硬件环境,只修改这些硬件相关的宏即可,这 些定义一般存储在头文件,即本例子中的“bsp_led.h”文件中,见代码清单 11-1。 代码清单 11-1 LED 控制引脚相关的宏 1 //引脚定义 2 /*******************************************************/ 3 //R 红色灯 4 #define LED1_PIN GPIO_Pin_10 5 #define LED1_GPIO_PORT GPIOH 6 #define LED1_GPIO_CLK RCC_AHB1Periph_GPIOH 7 8 //G 绿色灯 9 #define LED2_PIN GPIO_Pin_11 10 #define LED2_GPIO_PORT GPIOH 11 #define LED2_GPIO_CLK RCC_AHB1Periph_GPIOH 12 13 //B 蓝色灯 14 #define LED3_PIN GPIO_Pin_12 15 #define LED3_GPIO_PORT GPIOH 16 #define LED3_GPIO_CLK RCC_AHB1Periph_GPIOH 17 18 //小指示灯 19 #define LED4_PIN GPIO_Pin_11 20 #define LED4_GPIO_PORT GPIOD 21 #define LED4_GPIO_CLK RCC_AHB1Periph_GPIOD 22 /************************************************************/ 以上代码分别把控制四盏 LED 灯的 GPIO 端口、GPIO 引脚号以及 GPIO 端口时钟封装 起来了。在实际控制的时候我们就直接用这些宏,以达到应用代码硬件无关的效果。 其中的 GPIO 时钟宏“RCC_AHB1Periph_GPIOH”和“RCC_AHB1Periph_GPIOD”是 STM32 标准库定义的 GPIO 端口时钟相关的宏,它的作用与“GPIO_Pin_x”这类宏类似, 是用于指示寄存器位的,方便库函数使用。它们分别指示 GPIOH、GPIOD 的时钟,下面 初始化 GPIO 时钟的时候可以看到它的用法。 第 94 页 共 928 零死角玩转 STM32—F429 2. 控制 LED 灯亮灭状态的宏定义 为了方便控制 LED 灯,我们把 LED 灯常用的亮、灭及状态反转的控制也直接定义成 宏,见代码清单 11-2。 代码清单 11-2 控制 LED 亮灭的宏 1 2 /* 直接操作寄存器的方法控制 IO */ 3 #define digitalHi(p,i) {p->BSRRL=i;} //设置为高电平 4 #define digitalLo(p,i) {p->BSRRH=i;} //输出低电平 5 #define digitalToggle(p,i) {p->ODR ^=i;} //输出反转状态 6 7 8 /* 定义控制 IO 的宏 */ 9 #define LED1_TOGGLE digitalToggle(LED1_GPIO_PORT,LED1_PIN) 10 #define LED1_OFF digitalHi(LED1_GPIO_PORT,LED1_PIN) 11 #define LED1_ON digitalLo(LED1_GPIO_PORT,LED1_PIN) 12 13 #define LED2_TOGGLE digitalToggle(LED2_GPIO_PORT,LED2_PIN) 14 #define LED2_OFF digitalHi(LED2_GPIO_PORT,LED2_PIN) 15 #define LED2_ON digitalLo(LED2_GPIO_PORT,LED2_PIN) 16 17 #define LED3_TOGGLE digitalToggle(LED3_GPIO_PORT,LED3_PIN) 18 #define LED3_OFF digitalHi(LED3_GPIO_PORT,LED3_PIN) 19 #define LED3_ON digitalLo(LED3_GPIO_PORT,LED3_PIN) 20 21 #define LED4_TOGGLE digitalToggle(LED4_GPIO_PORT,LED4_PIN) 22 #define LED4_OFF digitalHi(LED4_GPIO_PORT,LED4_PIN) 23 #define LED4_ON digitalLo(LED4_GPIO_PORT,LED4_PIN) 24 25 26 /* 基本混色,后面高级用法使用 PWM 可混出全彩颜色,且效果更好 */ 27 28 //红 29 #define LED_RED \ 30 LED1_ON;\ 31 LED2_OFF;\ 32 LED3_OFF 33 34 //绿 35 #define LED_GREEN \ 36 LED1_OFF;\ 37 LED2_ON;\ 38 LED3_OFF 39 40 //蓝 41 #define LED_BLUE \ 42 LED1_OFF;\ 43 LED2_OFF;\ 44 LED3_ON 45 46 47 //黄(红+绿) 48 #define LED_YELLOW \ 49 LED1_ON;\ 50 LED2_ON;\ 51 LED3_OFF 这部分宏控制 LED 亮灭的操作是直接向 BSRR 寄存器写入控制指令来实现的,对 BSRRL 写 1 输出高电平,对 BSRRH 写 1 输出低电平,对 ODR 寄存器某位进行异或操作 可反转位的状态。 第 95 页 共 928 零死角玩转 STM32—F429 RGB 彩灯可以实现混色,如最后一段代码我们控制红灯和绿灯亮而蓝灯灭,可混出黄 色效果。 代码中的“\”是 C 语言中的续行符语法,表示续行符的下一行与续行符所在的代码是 同一行。代码中因为宏定义关键字“#define”只是对当前行有效,所以我们使用续行符来 连接起来,以下的代码是等效的: #define LED_YELLOW LED1_ON; LED2_ON; LED3_OFF 应用续行符的时候要注意,在“\”后面不能有任何字符(包括注释、空格),只能直接 回车。 3. LED GPIO 初始化函数 利用上面的宏,编写 LED 灯的初始化函数,见代码清单 11-3。 代码清单 11-3 LED GPIO 初始化函数 1 /** 2 * @brief 初始化控制 LED 的 IO 3 * @param 无 4 * @retval 无 5 */ 6 void LED_GPIO_Config(void) 7{ 8 /*定义一个 GPIO_InitTypeDef 类型的结构体*/ 9 GPIO_InitTypeDef GPIO_InitStructure; 10 11 /*开启 LED 相关的 GPIO 外设时钟*/ 12 RCC_AHB1PeriphClockCmd ( LED1_GPIO_CLK| 13 LED2_GPIO_CLK| 14 LED3_GPIO_CLK| 15 LED4_GPIO_CLK, 16 ENABLE); 17 18 /*选择要控制的 GPIO 引脚*/ 19 GPIO_InitStructure.GPIO_Pin = LED1_PIN; 20 21 /*设置引脚模式为输出模式*/ 22 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 23 24 /*设置引脚的输出类型为推挽输出*/ 25 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 26 27 /*设置引脚为上拉模式*/ 28 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; 29 30 /*设置引脚速率为 2MHz */ 31 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; 32 33 /*调用库函数,使用上面配置的 GPIO_InitStructure 初始化 GPIO*/ 34 GPIO_Init(LED1_GPIO_PORT, &GPIO_InitStructure); 35 36 /*选择要控制的 GPIO 引脚*/ 37 GPIO_InitStructure.GPIO_Pin = LED2_PIN; 38 GPIO_Init(LED2_GPIO_PORT, &GPIO_InitStructure); 39 40 /*选择要控制的 GPIO 引脚*/ 41 GPIO_InitStructure.GPIO_Pin = LED3_PIN; 42 GPIO_Init(LED3_GPIO_PORT, &GPIO_InitStructure); 43 第 96 页 共 928 44 45 46 47 48 49 50 51 52 53 } 零死角玩转 STM32—F429 /*选择要控制的 GPIO 引脚*/ GPIO_InitStructure.GPIO_Pin = LED4_PIN; GPIO_Init(LED4_GPIO_PORT, &GPIO_InitStructure); /*关闭 RGB 灯*/ LED_RGBOFF; /*指示灯默认开启*/ LED4(ON); 整个函数与“构建库函数雏形”章节中的类似,主要区别是硬件相关的部分使用宏来 代替,初始化 GPIO 端口时钟时也采用了 STM32 库函数,函数执行流程如下: (1) 使用 GPIO_InitTypeDef 定义 GPIO 初始化结构体变量,以便下面用于存储 GPIO 配置。 (2) 调用库函数 RCC_AHB1PeriphClockCmd 来使能 LED 灯的 GPIO 端口时钟,在前面的 章节中我们是直接向 RCC 寄存器赋值来使能时钟的,不如这样直观。该函数有两个输 入参数,第一个参数用于指示要配置的时钟,如本例中的“RCC_AHB1Periph_GPIOH” 和“RCC_AHB1Periph_GPIOD”,应用时我们使用“|”操作同时配置四个 LED 灯的 时钟;函数的第二个参数用于设置状态,可输入“Disable”关闭或“Enable”使能时 钟。 (3) 向 GPIO 初始化结构体赋值,把引脚初始化成推挽输出模式,其中的 GPIO_Pin 使用宏 “LEDx_PIN”来赋值,使函数的实现方便移植。 (4) 使用以上初始化结构体的配置,调用 GPIO_Init 函数向寄存器写入参数,完成 GPIO 的 初始化,这里的 GPIO 端口使用“LEDx_GPIO_PORT”宏来赋值,也是为了程序移植 方便。 (5) 使用同样的初始化结构体,只修改控制的引脚和端口,初始化其它 LED 灯使用的 GPIO 引脚。 (6) 使用宏控制 RGB 灯默认关闭,LED4 指示灯默认开启。 4. 主函数 编写完 LED 灯的控制函数后,就可以在 main 函数中测试了,见代码清单 11-4。 代码清单 11-4 控制 LED 灯 ,main 文件 1 #include "stm32f4xx.h" 2 #include "./led/bsp_led.h" 3 4 void Delay(__IO u32 nCount); 5 6 /** 7 * @brief 主函数 第 97 页 共 928 零死角玩转 STM32—F429 8 * @param 无 9 * @retval 无 10 */ 11 int main(void) 12 { 13 /* LED 端口初始化 */ 14 LED_GPIO_Config(); 15 16 /* 控制 LED 灯 */ 17 while (1) { 18 LED1( ON ); // 亮 19 Delay(0xFFFFFF); 20 LED1( OFF ); // 灭 21 22 LED2( ON ); // 亮 23 Delay(0xFFFFFF); 24 LED2( OFF ); // 灭 25 26 LED3( ON ); // 亮 27 Delay(0xFFFFFF); 28 LED3( OFF ); // 灭 29 30 LED4( ON ); // 亮 31 Delay(0xFFFFFF); 32 LED4( OFF ); // 灭 33 34 /*轮流显示 红绿蓝黄紫青白 颜色*/ 35 LED_RED; 36 Delay(0xFFFFFF); 37 38 LED_GREEN; 39 Delay(0xFFFFFF); 40 41 LED_BLUE; 42 Delay(0xFFFFFF); 43 44 LED_YELLOW; 45 Delay(0xFFFFFF); 46 47 LED_PURPLE; 48 Delay(0xFFFFFF); 49 50 LED_CYAN; 51 Delay(0xFFFFFF); 52 53 LED_WHITE; 54 Delay(0xFFFFFF); 55 56 LED_RGBOFF; 57 Delay(0xFFFFFF); 58 } 59 } 60 61 void Delay(__IO uint32_t nCount) //简单的延时函数 62 { 63 for (; nCount != 0; nCount--); 64 } 在 main 函数中,调用我们前面定义的 LED_GPIO_Config 初始化好 LED 的控制引脚, 然后直接调用各种控制 LED 灯亮灭的宏来实现 LED 灯的控制。 以上,就是一个使用 STM32 标准软件库开发应用的流程。 第 98 页 共 928 零死角玩转 STM32—F429 11.2.3 下载验证 把编译好的程序下载到开发板并复位,可看到 RGB 彩灯轮流显示不同的颜色。 11.3 STM32 标准库补充知识 1. SystemInit 函数去哪了? 在前几章我们自己建工程的时候需要定义一个 SystemInit 空函数,但是在这个用 STM32 标准库的工程却没有这样做,SystemInit 函数去哪了呢? 这个函数在 STM32 标准库的“system_stm32f4xx.c”文件中定义了,而我们的工程已 经包含该文件。标准库中的 SystemInit 函数把 STM32 芯片的系统时钟设置成了 180MHz, 即此时 AHB1 时钟频率为 180MHz,APB2 为 90MHz,APB1 为 45MHz。当 STM32 芯片上 电后,执行启动文件中的指令后,会调用该函数,设置系统时钟为以上状态。 2. 断言 细心对比过前几章我们自己定义的 GPIO_Init 函数与 STM32 标准库中同名函数的读者, 会发现标准库中的函数内容多了一些乱七八糟的东西,见代码清单 11-5。 代码清单 11-5 GPIO_Init 函数的断言部分 1 void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) 2{ 3 uint32_t pinpos = 0x00, pos = 0x00 , currentpin = 0x00; 4 5 /* Check the parameters */ 6 assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); 7 assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin)); 8 assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode)); 9 assert_param(IS_GPIO_PUPD(GPIO_InitStruct->GPIO_PuPd)); 10 11 /* ------- 以下内容省略,跟前面我们定义的函数内容相同----- */ 基本上每个库函数的开头都会有这样类似的内容,这里的“assert_param”实际是一个 宏,在库函数中它用于检查输入参数是否符合要求,若不符合要求则执行某个函数输出警 告,“assert_param”的定义见代码清单 11-6。 代码清单 11-6 stm32f4xx_conf.h 文件中关于断言的定义 1 2 #ifdef USE_FULL_ASSERT 3 /** 4 * @brief assert_param 宏用于函数的输入参数检查 5 * @param expr:若 expr 值为假,则调用 assert_failed 函数 6* 报告文件名及错误行号 7* 若 expr 值为真,则不执行操作 8 */ 9 #define assert_param(expr) \ 10 ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__)) 11 /* 错误输出函数 ------------------------------------------------------- */ 12 void assert_failed(uint8_t* file, uint32_t line); 13 #else 14 #define assert_param(expr) ((void)0) 第 99 页 共 928 15 #endif 零死角玩转 STM32—F429 这段代码的意思是,假如我们不定义“USE_FULL_ASSERT”宏,那么“assert_param” 就是一个空的宏(#else 与#endif 之间的语句生效),没有任何操作。从而所有库函数中的 assert_param 实际上都无意义,我们就当看不见好了。 假如我们定义了“USE_FULL_ASSERT”宏,那么“assert_param”就是一个有操作的 语句(#if 与#else 之间的语句生效),该宏对参数 expr 使用 C 语言中的问号表达式进行判断, 若 expr 值为真,则无操作(void 0),若表达式的值为假,则调用“assert_failed”函数,且该 函数的输入参数为“__FILE__”及“__LINE__”,这两个参数分别代表 “assert_param” 宏被调用时所在的“文件名”及“行号”。 但库文件只对“assert_failed”写了函数声明,没有写函数定义,实际用时需要用户来 定义,我们一般会用 printf 函数来输出这些信息,见代码清单 11-7。 代码清单 11-7 assert_failed 输出错误信息 1 void assert_failed(uint8_t* file, uint32_t line) 2{ 3 printf(“\r\n 输入参数错误,错误文件名=%s,行号=%s”,file,line); 4} 注意在我们的这个 LED 工程中,还不支持 printf 函数(在 USART 外设章节会讲解),想 测试 assert_failed 输出的读者,可以在这个函数中做点亮红色 LED 灯的操作,作为警告输 出测试。 那么为什么函数输入参数不对的时候,assert_param 宏中的 expr 参数值会是假呢?这 要回到 GPIO_Init 函数,看它对 assert_param 宏的调用,它被调用时分别以 “IS_GPIO_ALL_PERIPH(GPIOx)”、“IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin)”等作 为输入参数,也就是说被调用时,expr 实际上是一条针对输入参数的判断表达式。例如 “IS_GPIO_PIN”的宏定义: 1 #define IS_GPIO_PIN(PIN) ((PIN) != (uint32_t)0x00) 若它的输入参数 PIN 值为 0,则表达式的值为假,PIN 非 0 时表达式的值为真。我们 知道用于选择 GPIO 引脚号的宏“GPIO_Pin_x”的值至少有一个数据位为 1,这样的输入 参数才有意义,若 GPIO_InitStruct->GPIO_Pin 的值为 0,输入参数就无效了。配合 “IS_GPIO_PIN”这句表达式,“assert_param”就实现了检查输入参数的功能。对 assert_param 宏的其它调用方式类似,大家可以自己看库源码来研究一下。 3. Doxygen 注释规范 在 STM32 标准库以及我们自己编写的“bsp_led.c”文件中,可以看到一些比较特别的 注释,类似代码清单 11-8。 代码清单 11-8 Doxygen 注释规范 1 /** 2 * @brief 初始化控制 LED 的 IO 第 100 页 共 928 零死角玩转 STM32—F429 3 * @param 无 4 * @retval 无 5 */ 这是一种名为“Doxygen”的注释规范,如果在工程文件中按照这种规范去注释,可 以使用 Doxygen 软件自动根据注释生成帮助文档。我们所说非常重要的库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》,就是由该软件根据库文件的注释生成的。关于 Doxygen 注释规范本教程不作讲解,感兴趣的读者可自行搜索网络上的资料学习。 4. 防止头文件重复包含 在 STM32 标准库的所有头文件以及我们自己编写的“bsp_led.h”头文件中,可看到类 似代码清单 11-9 的宏定义。它的功能是防止头文件被重复包含,避免引起编译错误。 代码清单 11-9 防止头文件重复包含的宏 1 #ifndef __LED_H 2 #define __LED_H 3 4 /*此处省略头文件的具体内容*/ 5 6 #endif /* end of __LED_H */ 在头文件的开头,使用“#ifndef”关键字,判断标号“__LED_H”是否被定义,若没 有被定义,则从“#ifndef”至“#endif”关键字之间的内容都有效,也就是说,这个头文件 若被其它文件“#include”,它就会被包含到其该文件中了,且头文件中紧接着使用 “#define”关键字定义上面判断的标号“__LED_H”。当这个头文件被同一个文件第二次 “#include”包含的时候,由于有了第一次包含中的“#define __LED_H”定义,这时再判 断“#ifndef __LED_H”,判断的结果就是假了,从“#ifndef”至“#endif”之间的内容都 无效,从而防止了同一个头文件被包含多次,编译时就不会出现“redefine(重复定义)” 的错误了。 一般来说,我们不会直接在 C 的源文件写两个“#include”来包含同一个头文件,但 可能因为头文件内部的包含导致重复,这种代码主要是避免这样的问题。如“bsp_led.h” 文件中使用了“#include ―stm32f4xx.h‖ ”语句,按习惯,可能我们写主程序的时候会在 main 文件写“#include ―bsp_led.h‖ 及#include ―stm32f4xx.h‖”,这个时候“stm32f4xx.h”文 件就被包含两次了,如果没有这种机制,就会出错。 至于为什么要用两个下划线来定义“__LED_H”标号,其实这只是防止它与其它普通 宏定义重复了,如我们用“GPIO_PIN_0”来代替这个判断标号,就会因为 stm32f4xx.h 已 经定义了 GPIO_PIN_0,结果导致“bsp_led.h”文件无效了,“bsp_led.h”文件一次都没被 包含。 11.4 每课一练 1. 参考本章中的工程范例,使用 STM32 标准库编写控制 LED 灯的程序。 2. 修改“bsp_led.h”头文件中控制 LED 灯引脚的宏,改至实验板的其它 GPIO 引脚, 然后使用电压表测量该引脚的电平状态。(注意测量引脚状态的时候,程序要控制 第 101 页 共 928 零死角玩转 STM32—F429 GPIO 输出恒定的电平,方便电压表检测。部分引脚可能已连接到板子上的其它芯 片,可能存在干扰。) 3. 设置“stm32f4xx_conf.h”文件,使能“assert_param”断言功能,并定义 “assert_failed”函数,当调用库函数参数错误时,该函数点亮 LED 红灯警告。 第 102 页 共 928 零死角玩转 STM32—F429 第12章 GPIO 输入—按键检测 本章参考资料:《STM32F4xx 参考手册》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 按键检测使用到 GPIO 外设的基本输入功能,本章中不再赘述 GPIO 外设的概念,如 您忘记了,可重读前面“GPIO 框图剖析”小节,STM32 标准库中 GPIO 初始化结构体 GPIO_TypeDef 的定义与“定义引脚模式的枚举类型”小节中讲解的相同。 12.1 硬件设计 按键机械触点断开、闭合时,由于触点的弹性作用,按键开关不会马上稳定接通或一 下子断开,使用按键时会产生图 12-1 中的带波纹信号,需要用软件消抖处理滤波,不方便 输入检测。本实验板连接的按键带硬件消抖功能,见图 12-2,它利用电容充放电的延时, 消除了波纹,从而简化软件的处理,软件只需要直接检测引脚的电平即可。 图 12-1 按键抖动说明图 图 12-2 按键原理图 从按键的原理图可知,这些按键在没有被按下的时候,GPIO 引脚的输入状态为低电平 (按键所在的电路不通,引脚接地),当按键按下时,GPIO 引脚的输入状态为高电平(按键所 在的电路导通,引脚接到电源)。只要我们检测引脚的输入电平,即可判断按键是否被按下。 若您使用的实验板按键的连接方式或引脚不一样,只需根据我们的工程修改引脚即可, 程序的控制原理相同。 第 103 页 共 928 零死角玩转 STM32—F429 12.2 软件设计 同 LED 的工程,为了使工程更加有条理,我们把按键相关的代码独立分开存储,方便 以后移植。在“工程模板”之上新建“bsp_key.c”及“bsp_key.h”文件,这些文件也可根 据您的喜好命名,这些文件不属于 STM32 标准库的内容,是由我们自己根据应用需要编写 的。 12.2.1 编程要点 1. 使能 GPIO 端口时钟; 2. 初始化 GPIO 目标引脚为输入模式(引脚默认电平受按键电路影响,浮空/上拉/下拉均 没有区别); 3. 编写简单测试程序,检测按键的状态,实现按键控制 LED 灯。 12.2.2 代码分析 1. 按键引脚宏定义 同样,在编写按键驱动时,也要考虑更改硬件环境的情况。我们把按键检测引脚相关 的宏定义到 “bsp_key.h”文件中,见代码清单 11-1。 代码清单 12-1 按键检测引脚相关的宏 1 //引脚定义 2 /*******************************************************/ 3 #define KEY1_PIN GPIO_Pin_0 4 #define KEY1_GPIO_PORT GPIOA 5 #define KEY1_GPIO_CLK RCC_AHB1Periph_GPIOA 6 7 #define KEY2_PIN GPIO_Pin_13 8 #define KEY2_GPIO_PORT GPIOC 9 #define KEY2_GPIO_CLK RCC_AHB1Periph_GPIOC 10 /*******************************************************/ 以上代码根据按键的硬件连接,把检测按键输入的 GPIO 端口、GPIO 引脚号以及 GPIO 端口时钟封装起来了。 2. 按键 GPIO 初始化函数 利用上面的宏,编写按键的初始化函数,见代码清单 12-2。 代码清单 12-2 按键 GPIO 初始化函数 1 /** 2 * @brief 配置按键用到的 I/O 口 3 * @param 无 4 * @retval 无 5 */ 6 void Key_GPIO_Config(void) 7{ 8 GPIO_InitTypeDef GPIO_InitStructure; 9 10 /*开启按键 GPIO 口的时钟*/ 第 104 页 共 928 零死角玩转 STM32—F429 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 } RCC_AHB1PeriphClockCmd(KEY1_GPIO_CLK|KEY2_GPIO_CLK,ENABLE); /*选择按键的引脚*/ GPIO_InitStructure.GPIO_Pin = KEY1_PIN; /*设置引脚为输入模式*/ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; /*设置引脚不上拉也不下拉*/ GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; /*使用上面的结构体初始化按键*/ GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure); /*选择按键的引脚*/ GPIO_InitStructure.GPIO_Pin = KEY2_PIN; /*使用上面的结构体初始化按键*/ GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure); 同为 GPIO 的初始化函数,初始化的流程与“LED GPIO 初始化函数”章节中的类似,主 要区别是引脚的模式。函数执行流程如下: (1) 使用 GPIO_InitTypeDef 定义 GPIO 初始化结构体变量,以便下面用于存储 GPIO 配置。 (2) 调用库函数 RCC_AHB1PeriphClockCmd 来使能按键的 GPIO 端口时钟,调用时我们使 用“|”操作同时配置两个按键的时钟。 (3) 向 GPIO 初始化结构体赋值,把引脚初始化成浮空输入模式,其中的 GPIO_Pin 使用宏 “KEYx_PIN”来赋值,使函数的实现方便移植。由于引脚的默认电平受按键电路影 响,所以设置成“浮空/上拉/下拉”模式均没有区别。 (4) 使用以上初始化结构体的配置,调用 GPIO_Init 函数向寄存器写入参数,完成 GPIO 的 初始化,这里的 GPIO 端口使用“KEYx_GPIO_PORT”宏来赋值,也是为了程序移植 方便。 (5) 使用同样的初始化结构体,只修改控制的引脚和端口,初始化其它按键检测时使用的 GPIO 引脚。 3. 检测按键的状态 初始化按键后,就可以通过检测对应引脚的电平来判断按键状态了,见代码清单 12-3。 代码清单 12-3 检测按键的状态 1 /** 按键按下标置宏 2 * 按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0 3 * 若按键按下为低电平,把宏设置成 KEY_ON=0 ,KEY_OFF=1 即可 4 */ 5 #define KEY_ON 1 第 105 页 共 928 零死角玩转 STM32—F429 6 #define KEY_OFF 0 7 8 /** 9 * @brief 检测是否有按键按下 10 * @param GPIOx:具体的端口, x 可以是(A...K) 11 * @param GPIO_PIN:具体的端口位, 可以是 GPIO_PIN_x(x 可以是 0...15) 12 * @retval 按键的状态 13 * @arg KEY_ON:按键按下 14 * @arg KEY_OFF:按键没按下 15 */ 16 uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin) 17 { 18 /*检测是否有按键按下 */ 19 if (GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON ) { 20 /*等待按键释放 */ 21 while (GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON); 22 return KEY_ON; 23 } else 24 return KEY_OFF; 25 } 在这里我们定义了一个 Key_Scan 函数用于扫描按键状态。GPIO 引脚的输入电平可通 过读取 IDR 寄存器对应的数据位来感知,而 STM32 标准库提供了库函数 GPIO_ReadInputDataBit 来获取位状态,该函数输入 GPIO 端口及引脚号,函数返回该引脚 的电平状态,高电平返回 1,低电平返回 0。Key_Scan 函数中以 GPIO_ReadInputDataBit 的 返回值与自定义的宏“KEY_ON”对比,若检测到按键按下,则使用 while 循环持续检测 按键状态,直到按键释放,按键释放后 Key_Scan 函数返回一个“KEY_ON”值;若没有 检测到按键按下,则函数直接返回“KEY_OFF”。若按键的硬件没有做消抖处理,需要在 这个 Key_Scan 函数中做软件滤波,防止波纹抖动引起误触发。 4. 主函数 接下来我们使用主函数编写按键检测流程,见代码清单 12-4。 代码清单 12-4 按键检测主函数 1 /** 2 * @brief 主函数 3 * @param 无 4 * @retval 无 5 */ 6 int main(void) 7{ 8 /* LED 端口初始化 */ 9 LED_GPIO_Config(); 10 11 /*初始化按键*/ 12 Key_GPIO_Config(); 13 14 /* 轮询按键状态,若按键按下则反转 LED */ 15 while (1) { 16 if ( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON ) { 17 /*LED1 反转*/ 18 LED1_TOGGLE; 19 } 20 21 if ( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON ) { 第 106 页 共 928 零死角玩转 STM32—F429 22 /*LED2 反转*/ 23 LED2_TOGGLE; 24 } 25 } 26 } 代码中初始化 LED 灯及按键后,在 while 函数里不断调用 Key_Scan 函数,并判断其 返回值,若返回值表示按键按下,则反转 LED 灯的状态。 12.2.3 下载验证 把编译好的程序下载到开发板并复位,按下按键可以控制 LED 灯亮、灭状态。 12.3 每课一问 1. 工程中的 Key_Scan 函数使用 while 循环来阻塞检测,等待按键释放,若按键一直被按 下,会导致 CPU 无法进行其它操作,降低效率。尝试修改按键检测的方式,避免阻塞 等待。 第 107 页 共 928 零死角玩转 STM32—F429 第13章 GPIO—位带操作 本章参考资料:《STM32F4xx 中文参考手册》存储器和总线构架章节、GPIO 章节, 《Cortex®-M4 内核编程手册》2.2.5 Bit-banding。学习本章时,配套这些参考资料学习效果 会更佳。 13.1 位带简介 位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片 机中通过关键字 sbit 来实现位定义,F429 中没有这样的关键字,而是通过访问位带别名区 来实现。 在 F429 中,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,另一个是外 设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有 自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访 问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。 图 3 F429 位带地址 13.1.1 外设位带区 外设位带区的地址为:0X40000000~0X400F0000,大小为 1MB,这 1MB 的大小包含 了 APB1/2 和 AHB1 上所以外设的寄存器,AHB2/3 总线上的寄存器没有包括。AHB2 总线 上的外设地址范围为:0X50000000~0X50060BFF,AHB3 总线上的外设地址范围为: 第 108 页 共 928 零死角玩转 STM32—F429 0XA0000000~0XA0000FFF。外设位带区经过膨胀后的位带别名区地址为: 0X42000000~0X43FFFFFF,这部分地址空间为保留地址,没有跟任何的外设地址重合。 13.1.2 SRAM 位带区 SRAM 的位带区的地址为:0X2000 0000~X200F 0000,大小为 1MB,经过膨胀后的位 带别名区地址为:0X2200 0000~0X23FF FFFF,大小为 32MB。操作 SRAM 的比特位这个 用得很少。 13.1.3 位带区和位带别名区地址转换 位带区的一个比特位经过膨胀之后,虽然变大到 4 个字节,但是还是 LSB 才有效。有 人会问这不是浪费空间吗,要知道 F429 的系统总线是 32 位的,按照 4 个字节访问的时候 是最快的,所以膨胀成 4 个字节来访问是最高效的。 我们可以通过指针的形式访问位带别名区地址从而达到操作位带区比特位的效果。那 这两个地址直接如何转换,我们简单介绍一下。 1. 外设位带别名区地址 对于片上外设位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则 该比特在别名区的地址为: 1 AliasAddr= =0x42000000+ (A-0x40000000)*8*4 +n*4 0X42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址, (A-0x40000000)表示该比特前面有多少个字节,一个字节有 8 位,所以*8,一个位膨胀 后是 4 个字节,所以*4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字 节,所以也*4。 2. SRAM 位带别名区地址 对于 SRAM 位带区的某个比特,记它所在字节的地址为 A,位序号为 n(0<=n<=7),则 该比特在别名区的地址为: 1 AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4 公式分析同上。 3. 统一公式 为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址+位序号”转 换成别名区地址统一成一个宏。 1 // 把“位带地址+位序号”转换成别名地址的宏 第 109 页 共 928 零死角玩转 STM32—F429 2 #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2)) addr & 0xF0000000 是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2,如果是 外设,则取出的是 4,+0X02000000 之后就等于 0X42000000,0X42000000 是外设别名区 的起始地址。如果是 SRAM,则取出的是 2,+0X02000000 之后就等于 0X22000000, 0X22000000 是 SRAM 别名区的起始地址。 addr & 0x00FFFFFF 屏蔽了高三位,相当于减去 0X20000000 或者 0X40000000,但是 为什么是屏蔽高三位?因为外设的最高地址是:0X2010 0000,跟起始地址 0X20000000 相 减的时候,总是低 5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果, 具体屏蔽掉多少位跟最高地址有关。SRAM 同理分析即可。<<5 相当于*8*4,<<2 相当于 *4,这两个我们在上面分析过。 最后我们就可以通过指针的形式操作这些位带别名区地址,最终实现位带区的比特位 操作。 1 // 把一个地址转换成一个指针 2 #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) 3 4 // 把位带别名区地址转换成指针 5 #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum)) 13.2 GPIO 位带操作 外设的位带区,覆盖了全部的片上外设的寄存器,我们可以通过宏为每个寄存器的位 都定义一个位带别名地址,从而实现位操作。但这个在实际项目中不是很现实,也很少人 会这么做,我们在这里仅仅演示下 GPIO 中 ODR 和 IDR 这两个寄存器的位操作。 从手册中我们可以知道 ODR 和 IDR 这两个寄存器对应 GPIO 基址的偏移是 20 和 16, 我们先实现这两个寄存器的地址映射,其中 GPIOx_BASE 在库函数里面有定义。 1. GPIO 寄存器映射 代码 9 GPIO ODR 和 IDR 寄存器映射 1 // GPIO ODR 和 IDR 寄存器地址映射 2 #define GPIOA_ODR_Addr (GPIOA_BASE+20) 3 #define GPIOB_ODR_Addr (GPIOB_BASE+20) 4 #define GPIOC_ODR_Addr (GPIOC_BASE+20) 5 #define GPIOD_ODR_Addr (GPIOD_BASE+20) 6 #define GPIOE_ODR_Addr (GPIOE_BASE+20) 7 #define GPIOF_ODR_Addr (GPIOF_BASE+20) 8 #define GPIOG_ODR_Addr (GPIOG_BASE+20) 9 #define GPIOH_ODR_Addr (GPIOH_BASE+20) 10 #define GPIOI_ODR_Addr (GPIOI_BASE+20) 11 #define GPIOJ_ODR_Addr (GPIOJ_BASE+20) 12 #define GPIOK_ODR_Addr (GPIOK_BASE+20) 13 14 #define GPIOA_IDR_Addr (GPIOA_BASE+16) 15 #define GPIOB_IDR_Addr (GPIOB_BASE+16) 16 #define GPIOC_IDR_Addr (GPIOC_BASE+16) 17 #define GPIOD_IDR_Addr (GPIOD_BASE+16) 18 #define GPIOE_IDR_Addr (GPIOE_BASE+16) 19 #define GPIOF_IDR_Addr (GPIOF_BASE+16) 20 #define GPIOG_IDR_Addr (GPIOG_BASE+16) 21 #define GPIOH_IDR_Addr (GPIOH_BASE+16) 第 110 页 共 928 零死角玩转 STM32—F429 22 #define GPIOI_IDR_Addr 23 #define GPIOJ_IDR_Addr 24 #define GPIOK_IDR_Addr (GPIOI_BASE+16) (GPIOJ_BASE+16) (GPIOK_BASE+16) 现在我们就可以用位操作的方法来控制 GPIO 的输入和输出了,其中宏参数 n 表示具 体是哪一个 IO 口,n(0,1,2...16)。这里面包含了端口 A~K ,并不是每个单片机型号都有这 么多端口,使用这部分代码时,要查看你的单片机型号,如果是 176pin 的则最多只能使用 到 I 端口。 2. GPIO 位操作 代码 10 GPIO 输入输出位操作 1 // 单独操作 GPIO 的某一个 IO 口,n(0,1,2...16),n 表示具体是哪一个 IO 口 2 #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出 3 #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入 4 5 #define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出 6 #define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入 7 8 #define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出 9 #define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入 10 11 #define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出 12 #define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入 13 14 #define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出 15 #define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入 16 17 #define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出 18 #define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入 19 20 #define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出 21 #define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入 22 23 #define PHout(n) BIT_ADDR(GPIOH_ODR_Addr,n) //输出 24 #define PHin(n) BIT_ADDR(GPIOH_IDR_Addr,n) //输入 25 26 #define PIout(n) BIT_ADDR(GPIOI_ODR_Addr,n) //输出 27 #define PIin(n) BIT_ADDR(GPIOI_IDR_Addr,n) //输入 28 29 #define PJout(n) BIT_ADDR(GPIOJ_ODR_Addr,n) //输出 30 #define PJin(n) BIT_ADDR(GPIOJ_IDR_Addr,n) //输入 31 32 #define PKout(n) BIT_ADDR(GPIOK_ODR_Addr,n) //输出 33 #define PKin(n) BIT_ADDR(GPIOK_IDR_Addr,n) //输入 3. 主函数 该工程我们直接从 LED-库函数 操作移植过来,有关 LED GPIO 初始化和软件延时等 函数我们直接用,修改的是控制 GPIO 输出的部分改成了位操作。该实验我们让相应的 IO 口输出高低电平来控制 LED 的亮灭,负逻辑点亮。具体使用哪一个 IO 和点亮方式由硬件 平台决定。 代码 11 main 函数 第 111 页 共 928 零死角玩转 STM32—F429 1 int main(void) 2{ 3 /* LED 端口初始化 */ 4 LED_GPIO_Config(); 5 6 while (1) { 7 // PH10 = 0,点亮 LED 8 PHout(10)= 0; 9 SOFT_Delay(0x0FFFFF); 10 11 // PH10 = 1,熄灭 LED 12 PHout(10)= 1; 13 SOFT_Delay(0x0FFFFF); 14 } 15 16 } 13.3 每课一问 1、利用位带操作的方法,写一个 GPIO 输入的例程,比如按键采集。 2、如果使用的不是 GPIO 这个外设,而是其他的外设,那么公式该怎么改,比如要使 用的外设是 ADC。 第 112 页 共 928 零死角玩转 STM32—F429 第14章 启动文件详解 本章参考资料《STM32F4xx 中文参考手册》第十章-中断和事件:表 46. STM32F42xxx 和 STM32F43xxx 的向量表;MDK 中的帮助手册—ARM Development Tools: 用来查询 ARM 的汇编指令和编译器相关的指令。 14.1 启动文件简介 启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作: 1、初始化堆栈指针 SP=_initial_sp 2、初始化 PC 指针=Reset_Handler 3、初始化中断向量表 4、配置系统时钟 5、调用 C 库函数_main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界 14.2 查找 ARM 汇编指令 在讲解启动代码的时候,会涉及到 ARM 的汇编指令和 Cortex 内核的指令,有关 Cortex 内核的指令我们可以参考《CM3 权威指南 CnR2》第四章:指令集。剩下的 ARM 的 汇编指令我们可以在 MDK->Help->Uvision Help 中搜索到,以 EQU 为例,检索如下: 第 113 页 共 928 零死角玩转 STM32—F429 图 4 ARM 汇编指令索引 检索出来的结果会有很多,我们只需要看 Assembler User Guide 这部分即可。下面列 出了启动文件中使用到的 ARM 汇编指令,该列表的指令全部从 ARM Development Tools 这个帮助文档里面检索而来。其中编译器相关的指令 WEAK 和 ALIGN 为了方便也放在同 一个表格了。 表格 10 启动文件使用的 ARM 汇编指令汇总 指令名称 EQU AREA SPACE PRESERVE8 EXPORT DCD PROC WEAK IMPORT B ALIGN END 作用 给数字常量取一个符号名,相当于 C 语言中的 define 汇编一个新的代码段或者数据段 分配内存空间 当前文件堆栈需按照 8 字节对齐 声明一个标号具有全局属性,可被外部的文件使用 以字为单位分配内存,要求 4 字节对齐,并要求初始化这些内存 定义子程序,与 ENDP 成对使用,表示子程序结束 弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的 标号,如果外部文件没有定义也不出错。要注意的是:这个不是 ARM 的指令,是编译器的,这里放在一起只是为了方便。 声明标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似 跳转到一个标号 编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即 数,缺省表示 4 字节对齐。要注意的是:这个不是 ARM 的指令,是 编译器的,这里放在一起只是为了方便。 到达文件的末尾,文件结束 第 114 页 共 928 零死角玩转 STM32—F429 IF,ELSE,ENDIF 汇编条件分支语句,跟 C 语言的 if else 类似 14.3 启动文件代码讲解 1. Stack—栈 1 Stack_Size 2 3 4 Stack_Mem 5 __initial_sp EQU AREA SPACE 0x00000400 STACK, NOINIT, READWRITE, ALIGN=3 Stack_Size 开辟栈的大小为 0X00000400(1KB),名字为 STACK,NOINIT 即不初始化,可读可 写,8(2^3)字节对齐。 栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部 SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。 如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault 的时候,这时你就要考 虑下是不是栈不够大,溢出了。 EQU:宏定义的伪指令,相当于等于,类似与 C 中的 define。 AREA:告诉汇编器汇编一个新的代码段或者数据段。STACK 表示段名,这个可以任 意命名;NOINIT 表示不初始化;READWRITE 表示可读可写,ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。 SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。 标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由 高向低生长的。 2. Heap 堆 1 Heap_Size 2 3 4 __heap_base 5 Heap_Mem 6 __heap_limit EQU AREA SPACE 0x00000200 HEAP, NOINIT, READWRITE, ALIGN=3 Heap_Size 开辟堆的大小为 0X00000200(512 字节),名字为 HEAP,NOINIT 即不初始化,可 读可写,8(2^3)字节对齐。__heap_base 表示对的起始地址,__heap_limit 表示堆的结束 地址。堆是由低向高生长的,跟栈的生长方向相反。 堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆上面。这个在 STM32 里面用的比较少。 1 PRESERVE8 2 THUMB PRESERVE8:指定当前文件的堆栈按照 8 字节对齐。 THUMB:表示后面指令兼容 THUMB 指令。THUBM 是 ARM 以前的指令集,16bit, 现在 Cortex-M 系列的都使用 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位 的指令,是 THUMB 的超级。 第 115 页 共 928 零死角玩转 STM32—F429 3. 向量表 1 AREA 2 EXPORT 3 EXPORT 4 EXPORT RESET, DATA, READONLY __Vectors __Vectors_End __Vectors_Size 定义一个数据段,名字为 RESET,可读。并声明 __Vectors、__Vectors_End 和 __Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。 EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编 译器,则使用的是 GLOBAL 这个指令。 当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR 的入口地址, 内核使用了―向量表查表机制‖。这里使用一张向量表。向量表其实是一个 WORD( 32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地 址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向 量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0)处必须包 含一张向量表,用于初始时的异常分配。要注意的是这里有个另类: 0 号类型并不是什么 入口地址,而是给出了复位后 MSP 的初值。 表格 11 F429 向量表 编 优 优先级 名称 说明 地址 号 先 类型 级 -- - 保留(实际存的是 MSP 地址) 0X0000 0000 -3 固定 Reset 复位 0X0000 0004 -2 固定 NMI 不可屏蔽中断。 RCC 时钟安全系统 0X0000 0008 (CSS) 连接到 NMI 向量 -1 固定 HardFault 所有类型的错误 0X0000 000C 0 可编程 MemManage 存储器管理 0X0000 0010 1 可编程 BusFault 预取指失败,存储器访问失败 0X0000 0014 2 可编程 UsageFault 未定义的指令或非法状态 0X0000 0018 -- - 保留 0X0000 001C- 0X0000 002B 3 可编程 SVCall 通过 SWI 指令调用的系统服务 0X0000 002C 4 可编程 Debug Monitor 调试监控器 0X0000 0030 -- - 保留 0X0000 0034 5 可编程 PendSV 可挂起的系统服务 0X0000 0038 6 可编程 SysTick 系统嘀嗒定时器 0X0000 003C 0 7 可编程 - 窗口看门狗中断 0X0000 0040 1 8 可编程 PVD 连接 EXTI 线的可编程电压检测中断 0X0000 0044 2 9 可编程 TAMP_STAMP 连接 EXTI 线的入侵和时间戳中断 0X0000 0048 中间部分省略,详情请参考 STM32F4xx 中文参考手册》第十章-中断和事件-向量表部分 84 91 可编程 SPI4 SPI4 全局中断 0X0000 0190 85 92 可编程 SPI5 SPI5 全局中断 0X0000 0194 86 93 可编程 SPI6 SPI6 全局中断 0X0000 0198 87 94 可编程 SAI1 SAI1 全局中断 0X0000 019C 88 95 可编程 LTDC LTDC 全局中断 0X0000 01A0 第 116 页 共 928 89 96 可编程 90 97 可编程 零死角玩转 STM32—F429 LTDC_ER DMA2D LTDC_ER 全局中断 DMA2D 全局中断 代码 12 向量表 1 __Vectors DCD __initial_sp ;栈顶地址 2 DCD Reset_Handler ;复位程序地址 3 DCD NMI_Handler 4 DCD HardFault_Handler 5 DCD MemManage_Handler 6 DCD BusFault_Handler 7 DCD UsageFault_Handler 8 DCD 0 ; 0 表示保留 9 DCD 0 10 DCD 0 11 DCD 0 12 DCD SVC_Handler 13 DCD DebugMon_Handler 14 DCD 0 15 DCD PendSV_Handler 16 DCD SysTick_Handler 17 18 19 ;外部中断开始 20 DCD WWDG_IRQHandler 21 DCD PVD_IRQHandler 22 DCD TAMP_STAMP_IRQHandler 23 24 ;限于篇幅,中间代码省略 25 DCD LTDC_IRQHandler 26 DCD LTDC_ER_IRQHandler 27 DCD DMA2D_IRQHandler 28 __Vectors_End 1 __Vectors_Size EQU __Vectors_End - __Vectors 0X0000 01A4 0X0000 01A8 __Vectors 为向量表起始地址,__Vectors_End 为向量表结束地址,两个相减即可算出向量 表大小。 向量表从 FLASH 的 0 地址开始放置,以 4 个字节为一个单位,地址 0 存放的是栈 顶地址,0X04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都 是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。 DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内 存。在向量表中,DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。 4. 复位程序 1 AREA |.text|, CODE, READONLY 定义一个名称为.text 的代码段,可读。 1 Reset_Handler PROC 2 EXPORT 3 IMPORT 4 IMPORT 5 6 LDR Reset_Handler SystemInit __main R0, =SystemInit [WEAK] 第 117 页 共 928 零死角玩转 STM32—F429 7 BLX R0 8 LDR R0, =__main 9 BX R0 10 ENDP 复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟, 然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界。 WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部 文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并 不是唯一的。 IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表 示 SystemInit 和__main 这两个函数均来自外部的文件。 SystemInit()是一个标准的库函数,在 system_stm32f4xx.c 这个库文件总定义。主要作 用是配置系统时钟,这里调用这个函数之后,F429 的系统时钟配被配置为 180M。 __main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,最终调用 main 函数去 到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因。如果我们在这里不 调用__main,那么程序最终就不会调用我们 C 文件里面的 main,如果是调皮的用户就可以 修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。 1 Reset_Handler PROC 2 EXPORT 3 IMPORT 4 IMPORT 5 6 LDR 7 BLX 8 LDR 9 BX 10 ENDP Reset_Handler SystemInit user_main R0, =SystemInit R0 R0, =user_main R0 [WEAK] 这个时候你在 C 文件里面写的主函数名称就不是 main 了,而是 user_main 了。 LDR、BLX、BX 是 CM4 内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里 面查询到,具体作用见下表: 指令名称 LDR BL BLX BX 作用 从存储器中加载字到一个寄存器中 跳转到由寄存器/标号给出的地址,并把跳转前的下条指令地址保存到 LR 跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要 把跳转前的下条指令地址保存到 LR 跳转到由寄存器/标号给出的地址,不用返回 5. 中断服务程序 在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务 函数不一样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里 面重新实现,这里只是提前占了一个位置而已。 如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务 程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断 服务程序中,并且在这个空函数中无线循环,即程序就死在这里。 1 NMI_Handler 2 PROC ;系统异常 EXPORT NMI_Handler [WEAK] 第 118 页 共 928 零死角玩转 STM32—F429 3 B 4 ENDP 5 6 ;限于篇幅,中间代码省略 7 SysTick_Handler PROC 8 EXPORT 9 B 10 ENDP 11 12 Default_Handler PROC 13 EXPORT 14 EXPORT 15 EXPORT 16 17 ;限于篇幅,中间代码省略 18 LTDC_IRQHandler 19 LTDC_ER_IRQHandler 20 DMA2D_IRQHandler 21 B 22 ENDP . SysTick_Handler . [WEAK] ;外部中断 WWDG_IRQHandler [WEAK] PVD_IRQHandler [WEAK] TAMP_STAMP_IRQHandler [WEAK] . B:跳转到一个标号。这里跳转到一个‘.’,即表示无线循环。 6. 用户堆栈初始化 1 ALIGN ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字 节对齐。 1 ;用户栈和堆初始化 2 IF :DEF:__MICROLIB 3 4 EXPORT __initial_sp 5 EXPORT __heap_base 6 EXPORT __heap_limit 7 8 ELSE 9 10 IMPORT __use_two_region_memory 11 EXPORT __user_initial_stackheap 12 13 __user_initial_stackheap 14 15 LDR R0, = Heap_Mem 16 LDR R1, =(Stack_Mem + Stack_Size) 17 LDR R2, = (Heap_Mem + Heap_Size) 18 LDR R3, = Stack_Mem 19 BX LR 20 21 ALIGN 22 23 ENDIF 24 END 判断是否定义了__MICROLIB ,如果定义了则赋予标号__initial_sp(栈顶地址)、 __heap_base(堆起始地址)、__heap_limit(堆结束地址)全局属性,可供外部文件调用。 如果没有定义(实际的情况就是我们没定义__MICROLIB)则使用默认的 C 库,然后初始 化用户堆栈大小,这部分有 C 库函数__main 来完成,当初始化完堆栈之后,就调用 main 函数去到 C 的世界。 第 119 页 共 928 零死角玩转 STM32—F429 IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似 END:文件结束 14.4 系统启动流程 下面这段话引用自《CM3 权威指南 CnR2》3.8—复位序列,CM4 的复位序列跟 CM3 一样。—秉火 注。 在离开复位状态后, CM3 做的第一件事就是读取下列两个 32 位整数的值: 1、从地址 0x0000,0000 处取出 MSP 的初始值。 2、从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量, LSB 必须是 1。 然后从这个值所对应的地址处取指。 图 5 复位序列 请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统 的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。 在 CM3 中,在 0 地址处提供 MSP 的初始值,然后紧跟着就是向量表。 向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是 我们刚刚分析的 Reset_Handler 这个函数。 第 120 页 共 928 零死角玩转 STM32—F429 图 6 初始化 MSP 和 PC 的一个范例 因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。举例 来说,如果我们的堆栈区域在 0x20007C00-0x20007FFF 之间,那么 MSP 的初始值 就必须是 0x20008000。 向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。要注意因为 CM3 是在 Thumb 态下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为 这个原因,图 6 中使用 0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式 开始了程序的执行(即去到 C 的世界)。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没来得及执行,就发生了 NMI 或是其它 fault。 MSP 初始化好后就已经为它们的 服务例程准备好了堆栈。 现在,程序就进入了我们熟悉的 C 世界,现在我们也应该明白 main 并不是系统执 行的第一个程序了。 14.5 每课一问 1、启动文件的主要作用是什么? 2、FLASH 地址 0 存放的是什么? 3、熟悉启动文件里面的 ARM 汇编指令 第 121 页 共 928 第15章 零死角玩转 STM32—F429 RCC—使用 HSE/HSI 配置时钟 本章参考资料:《STM32F4xx 中文参考手册》RCC 章节。 学习本章时,配合《STM32F4xx 中文参考手册》RCC 章节一起阅读,效果会更佳,特 别是涉及到寄存器说明的部分。 RCC :reset clock control 复位和时钟控制器。本章我们主要讲解时钟部分,特别是要 着重理解时钟树,理解了时钟树,F429 的一切时钟的来龙去脉都会了如指掌。 15.1 RCC 主要作用—时钟部分 设置系统时钟 SYSCLK、设置 AHB 分频因子(决定 HCLK 等于多少)、设置 APB2 分 频因子(决定 PCLK2 等于多少)、设置 APB1 分频因子(决定 PCLK1 等于多少)、设置各 个外设的分频因子;控制 AHB、APB2 和 APB1 这三条总线时钟的开启、控制每个外设的时 钟的开启。对于 SYSCLK、HCLK、PCLK2、PCLK1 这四个时钟的配置一般是:HCLK = SYSCLK=PLLCLK = 180M,PCLK1=HCLK/2 = 90M,PCLK1=HCLK/4 = 45M。这个时钟配 置也是库函数的标准配置,我们用的最多的就是这个。 15.2 RCC 框图剖析—时钟树 时钟树单纯讲理论的话会比较枯燥,如果选取一条主线,并辅以代码,先主后次讲解 的话会很容易,而且记忆还更深刻。我们这里选取库函数时钟系统时钟函数: SetSysClock(); 以这个函数的编写流程来讲解时钟树,这个函数也是我们用库的时候默认 的系统时钟设置函数。该函数的功能是利用 HSE 把时钟设置为:HCLK = SYSCLK=PLLCLK = 180M,PCLK1=HCLK/2 = 90M,PCLK1=HCLK/4 = 45M 下面我们就以这个代码的流程 为主线,来分析时钟树,对应的是图中的黄色部分,代码流程在时钟树中以数字的大小顺 序标识。 第 122 页 共 928 零死角玩转 STM32—F429 图 7 STM32F429 时钟树 第 123 页 共 928 零死角玩转 STM32—F429 15.2.1 系统时钟 1. ①HSE 高速外部时钟信号 HSE 是高速的外部时钟信号,可以由有源晶振或者无源晶振提供,频率从 4-26MHZ 不等。当使用有源晶振时,时钟从 OSC_IN 引脚进入,OSC_OUT 引脚悬空,当选用无源 晶振时,时钟从 OSC_IN 和 OSC_OUT 进入,并且要配谐振电容。HSE 我们使用 25M 的无 源晶振。如果我们使用 HSE 或者 HSE 经过 PLL 倍频之后的时钟作为系统时钟 SYSCLK, 当 HSE 故障时候,不仅 HSE 会被关闭,PLL 也会被关闭,此时高速的内部时钟时钟信号 HSI 会作为备用的系统时钟,直到 HSE 恢复正常,HSI=16M。 2. ②锁相环 PLL PLL 的主要作用是对时钟进行倍频,然后把时钟输出到各个功能部件。PLL 有两个, 一个是主 PLL,另外一个是专用的 PLLI2S,他们均由 HSE 或者 HSI 提供时钟输入信号。 主 PLL 有两路的时钟输出,第一个输出时钟 PLLCLK 用于系统时钟,F429 里面最高 是 180M,第二个输出用于 USB OTG FS 的时钟(48M)、RNG 和 SDIO 时钟(<=48M)。 专用的 PLLI2S 用于生成精确时钟,给 I2S 提供时钟。 HSE 或者 HSI 经过 PLL 时钟输入分频因子 M(2~63)分频后,成为 VCO 的时钟输入, VCO 的时钟必须在 1~2M 之间,我们选择 HSE=25M 作为 PLL 的时钟输入,M 设置为 25, 那么 VCO 输入时钟就等于 1M。 VCO 输入时钟经过 VCO 倍频因子 N 倍频之后,成为 VCO 时钟输出,VCO 时钟必须 在 192~432M 之间。我们配置 N 为 360,则 VCO 的输出时钟等于 360M。如果要把系统时 钟超频,就得在 VCO 倍频系数 N 这里做手脚。PLLCLK_OUTMAX = VCOCLK_OUTMAX/P_MIN = 432/2=216M,即 F429 最高可超频到 216M。 VCO 输出时钟之后有三个分频因子:PLLCLK 分频因子 p,USB OTG FS/RNG/SDIO 时钟分频因子 Q,分频因子 R(F446 才有,F429 没有)。p 可以取值 2、4、6、8,我们配 置为 2,则得到 PLLCLK=180M。Q 可以取值 4~15,但是 USB OTG FS 必须使用 48M, Q=VCO 输出时钟 360/48=7.5,出现了小数这明显是错误,权衡之策是是重新配置 VCO 的 倍频因子 N=336,VCOCLK=1M*336=336M,PLLCLK=VCOCLK/2=168M, USBCLK=336/7=48M,细心的读者应该发现了,在使用 USB 的时候,PLLCLK 被降低到 了 168M,不能使用 180M,这实乃 ST 的一个奇葩设计。有关 PLL 的配置有一个专门的 RCC PLL 配置寄存器 RCC_PLLCFGR,具体描述看手册即可。 PLL 的时钟配置经过,稍微整理下可由如下公式表达: 第 124 页 共 928 零死角玩转 STM32—F429 VCOCLK_IN = PLLCLK_IN / M = HSE / 25 = 1M VCOCLK_OUT = VCOCLK_IN * N = 1M * 360 = 360M PLLCLK_OUT=VCOCLK_OUT/P=360/2=180M USBCLK = VCOCLK_OUT/Q=360/7=51.7。暂时这样配置,到真正使用 USB 的时候会 重新配置。 3. ③系统时钟 SYSCLK 系统时钟来源可以是:HSI、PLLCLK、HSE,具体的由时钟配置寄存器 RCC_CFGR 的 SW 位配置。我们这里设置系统时钟:SYSCLK = PLLCLK = 180M。如果系统时钟是由 HSE 经过 PLL 倍频之后的 PLLCLK 得到,当 HSE 出现故障的时候,系统时钟会切换为 HSI=16M,直到 HSE 恢复正常为止。 4. ④AHB 总线时钟 HCLK 系统时钟 SYSCLK 经过 AHB 预分频器分频之后得到时钟叫 APB 总线时钟,即 HCLK, 分频因子可以是:[1,2,4,8,16,64,128,256,512],具体的由时钟配置寄存器 RCC_CFGR 的 HPRE 位设置。片上大部分外设的时钟都是经过 HCLK 分频得到,至于 AHB 总线上的外设的时钟设置为多少,得等到我们使用该外设的时候才设置,我们这里只需粗 线条的设置好 APB 的时钟即可。我们这里设置为 1 分频,即 HCLK=SYSCLK=180M。 5. ⑤APB2 总线时钟 HCLK2 APB2 总线时钟 PCLK2 由 HCLK 经过高速 APB2 预分频器得到,分频因子可以 是:[1,2,4,8,16],具体由时钟配置寄存器 RCC_CFGR 的 PPRE2 位设置。HCLK2 属于高 速的总线时钟,片上高速的外设就挂载到这条总线上,比如全部的 GPIO、USART1、SPI1 等。至于 APB2 总线上的外设的时钟设置为多少,得等到我们使用该外设的时候才设置, 我们这里只需粗线条的设置好 APB2 的时钟即可。我们这里设置为 2 分频,即 PCLK2 = HCLK /2= 90M。 6. ⑥APB1 总线时钟 HCLK1 APB1 总线时钟 PCLK1 由 HCLK 经过低速 APB 预分频器得到,分频因子可以是:[1,2,4, 8,16],具体由时钟配置寄存器 RCC_CFGR 的 PPRE1 位设置。 HCLK1 属于低速的总线时钟,最高为 45M,片上低速的外设就挂载到这条总线上,比如 USART2/3/4/5、SPI2/3,I2C1/2 等。至于 APB1 总线上的外设的时钟设置为多少,得等到 第 125 页 共 928 零死角玩转 STM32—F429 我们使用该外设的时候才设置,我们这里只需粗线条的设置好 APB1 的时钟即可。我们这 里设置为 4 分频,即 PCLK1 = HCLK/4 = 45M。 7. 设置系统时钟库函数 上面的 6 个步骤对应的设置系统时钟库函数如下,为了方便阅读,已经把跟 429 不相 关的代码删掉,把英文注释翻译成了中文,并把代码标上了序号,总共 6 个步骤。该函数 是直接操作寄存器的,有关寄存器部分请参考数据手册的 RCC 的寄存器描述部分。 代码 13 设置系统时钟库函数 1 /* 2 * 使用 HSE 时,设置系统时钟的步骤 3 * 1、开启 HSE ,并等待 HSE 稳定 4 * 2、设置 AHB、APB2、APB1 的预分频因子 5 * 3、设置 PLL 的时钟来源 6 * 设置 VCO 输入时钟 分频因子 m 7 * 设置 VCO 输出时钟 倍频因子 n 8 * 设置 PLLCLK 时钟分频因子 p 9 * 设置 OTG FS,SDIO,RNG 时钟分频因子 q 10 * 4、开启 PLL,并等待 PLL 稳定 11 * 5、把 PLLCK 切换为系统时钟 SYSCLK 12 * 6、读取时钟切换状态位,确保 PLLCLK 被选为系统时钟 13 */ 14 15 #define PLL_M 25 16 #define PLL_N 360 17 #define PLL_P 2 18 #define PLL_Q 7 如果要超频的话,修改 PLL_N 这个宏即可,取值范围为:192~432。 1 void SetSysClock(void) 2{ 3 4 __IO uint32_t StartUpCounter = 0, HSEStatus = 0; 5 6 // ①使能 HSE 7 RCC->CR |= ((uint32_t)RCC_CR_HSEON); 8 9 // 等待 HSE 启动稳定 10 do { 11 HSEStatus = RCC->CR & RCC_CR_HSERDY; 12 StartUpCounter++; 13 } while ((HSEStatus==0)&&(StartUpCounter 14 !=HSE_STARTUP_TIMEOUT)); 15 16 if ((RCC->CR & RCC_CR_HSERDY) != RESET) { 17 HSEStatus = (uint32_t)0x01; 18 } else { 19 HSEStatus = (uint32_t)0x00; 20 } 21 22 // HSE 启动成功 23 if (HSEStatus == (uint32_t)0x01) { 24 // 调压器电压输出级别配置为 1,以便在器件为最大频率 25 // 工作时使性能和功耗实现平衡 26 RCC->APB1ENR |= RCC_APB1ENR_PWREN; 第 126 页 共 928 零死角玩转 STM32—F429 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 } PWR->CR |= PWR_CR_VOS; // ②设置 AHB/APB2/APB1 的分频因子 // HCLK = SYSCLK / 1 RCC->CFGR |= RCC_CFGR_HPRE_DIV1; // PCLK2 = HCLK / 2 RCC->CFGR |= RCC_CFGR_PPRE2_DIV2; // PCLK1 = HCLK / 4 RCC->CFGR |= RCC_CFGR_PPRE1_DIV4; // ③配置主 PLL 的时钟来源,设置 M,N,P,Q // Configure the main PLL RCC->PLLCFGR = PLL_M|(PLL_N<<6)| (((PLL_P >> 1) -1) << 16) | (RCC_PLLCFGR_PLLSRC_HSE) | (PLL_Q << 24); // ④使能主 PLL RCC->CR |= RCC_CR_PLLON; // 等待 PLL 稳定 while ((RCC->CR & RCC_CR_PLLRDY) == 0) { } /*----------------------------------------------------*/ // 开启 OVER-RIDE 模式,以能达到更改频率 PWR->CR |= PWR_CR_ODEN; while ((PWR->CSR & PWR_CSR_ODRDY) == 0) { } PWR->CR |= PWR_CR_ODSWEN; while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) { } // 配置 FLASH 预取指,指令缓存,数据缓存和等待状态 FLASH->ACR = FLASH_ACR_PRFTEN |FLASH_ACR_ICEN |FLASH_ACR_DCEN |FLASH_ACR_LATENCY_5WS; /*---------------------------------------------------*/ // ⑤选择主 PLLCLK 作为系统时钟源 RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC->CFGR |= RCC_CFGR_SW_PLL; // ⑥读取时钟切换状态位,确保 PLLCLK 选为系统时钟 while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS ) != RCC_CFGR_SWS_PLL); { } } else { // HSE 启动出错处理 } 15.2.2 其他时钟 通过对系统时钟设置的讲解,整个时钟树我们已经把握的有六七成,剩下的时钟部分 我们讲解几个重要的。 第 127 页 共 928 零死角玩转 STM32—F429 1. A、RTC 时钟 RTCCLK 时钟源可以是 HSE 1 MHz( HSE 由一个可编程的预分频器分频)、 LSE 或 者 LSI 时钟。选择方式是编程 RCC 备份域控制寄存器 (RCC_BDCR) 中的 RTCSEL[1:0] 位 和 RCC 时钟配置寄存器 (RCC_CFGR) 中的 RTCPRE[4:0] 位。所做的选择只能通过复位备 份域的方式修改。我们通常的做法是由 LSE 给 RTC 提供时钟,大小为 32.768KHZ。LSE 由外接的晶体谐振器产生,所配的谐振电容精度要求高,不然很容易不起震。 2. B、独立看门狗时钟 独立看门狗时钟由内部的低速时钟 LSI 提供,大小为 32KHZ。 3. C、I2S 时钟 I2S 时钟可由外部的时钟引脚 I2S_CKIN 输入,也可由专用的 PLLI2SCLK 提供,具体 的由 RCC 时钟配置寄存器 (RCC_CFGR)的 I2SSCR 位配置。我们在使用 I2S 外设驱动 W8978 的时候,使用的时钟是 PLLI2SCLK,这样就可以省掉一个有源晶振。 4. D、PHY 以太网时钟 F429 要想实现以太网功能,除了有本身内置的 MAC 之外,还需要外接一个 PHY 芯片, 常见的 PHY 芯片有 DP83848 和 LAN8720,其中 DP83848 支持 MII 和 RMII 接口, LAN8720 只支持 RMII 接口。秉火 F429 开发板用的是 RMII 接口,选择的 PHY 芯片是 LAB8720。使用 RMII 接口的好处是使用的 IO 减少了一半,速度还是跟 MII 接口一样。当 使用 RMII 接口时,PHY 芯片只需输出一路时钟给 MCU 即可,如果是 MII 接口,PHY 芯 片则需要提供两路时钟给 MCU。 5. E、USB PHY 时钟 F429 的 USB 没有集成 PHY,要想实现 USB 高速传输的话,必须外置 USB PHY 芯片, 常用的芯片是 USB3300。当外接 USB PHY 芯片时,PHY 芯片需要给 MCU 提供一个时钟。 外扩 USB3300 会占用非常多的 IO,跟 SDRAM 和 RGB888 的 IO 会复用的很厉害,鉴 于 USB 高速传输用的比较少,秉火 429 就没有外扩这个芯片。 第 128 页 共 928 零死角玩转 STM32—F429 6. F、MCO 时钟输出 MCO 是 microcontroller clock output 的缩写,是微控制器时钟输出引脚,主要作用是可 以对外提供时钟,相当于一个有源晶振。F429 中有两个 MCO,由 PA8/PC9 复用所得。 MCO1 所需的时钟源通过 RCC 时钟配置寄存器 (RCC_CFGR) 中的 MCO1PRE[2:0] 和 MCO1[1:0]位选择。MCO2 所需的时钟源通过 RCC 时钟配置寄存器 (RCC_CFGR) 中的 MCO2PRE[2:0] 和 MCO2 位选择。有关 MCO 的 IO、时钟选择和输出速率的具体信息如下 表所示: 时钟输出 IO MCO1 PA8 MCO2 PC9 时钟来源 HSI、LSE、HSE、PLLCLK HSE、PLLCLK、SYSCLK、PLLI2SCLK 最大输出速率 100M 100M 15.3 配置系统时钟实验 15.3.1 使用 HSE 一般情况下,我们都是使用 HSE,然后 HSE 经过 PLL 倍频之后作为系统时钟。F429 系统时钟最高为 180M,这个是官方推荐的最高的稳定时钟,如果你想铤而走险,也可以 超频,超频最高能到 216M。 如果我们使用库函数编程,当程序来到 main 函数之前,启动文件: startup_stm32f429_439xx.s 已经调用 SystemInit()函数把系统时钟初始化成 180MHZ, SystemInit()在库文件:system_stm32f4xx.c 中定义。如果我们想把系统时钟设置低一点或者 超频的话,可以修改底层的库文件,但是为了维持库的完整性,我们可以根据时钟树的流 程自行写一个。 15.3.2 使用 HSI 当 HSE 直接或者间接(HSE 经过 PLL 倍频)的作为系统时钟的时候,如果 HSE 发生 故障,不仅 HSE 会被关闭,连 PLL 也会被关闭,这个时候系统会自动切换 HSI 作为系统时 钟,此时 SYSCLK=HSI=16M,如果没有开启 CSS 和 CSS 中断的话,那么整个系统就只能 在低速率运行,这是系统跟瘫痪没什么两样。 如果开启了 CSS 功能的话,那么可以当 HSE 故障时,在 CSS 中断里面采取补救措施, 使用 HSI,重新设置系统频率为 180M,让系统恢复正常使用。但这只是权宜之计,并非万 全之策,最好的方法还是要采取相应的补救措施并报警,然后修复 HSE。临时使用 HSI 只 是为了把损失降低到最小,毕竟 HSI 较于 HSE 精度还是要低点。 第 129 页 共 928 零死角玩转 STM32—F429 F103 系列中,使用 HSI 最大只能把系统设置为 64M,并不能跟使用 HSE 一样把系统 时钟设置为 72M,究其原因是 HSI 在进入 PLL 倍频的时候必须 2 分频,导致 PLL 倍频因子 调到最大也只能到 64M,而 HSE 进入 PLL 倍频的时候则不用 2 分频。 在 F429 中,无论是使用 HSI 还是 HSE 都可以把系统时钟设置为 180M,因为 HSE 或 者 HSI 在进入 PLL 倍频的时候都会被分频为 1M 之后再倍频。 还有一种情况是,有些用户不想用 HSE,想用 HSI,但是又不知道怎么用 HSI 来设置 系统时钟,因为调用库函数都是使用 HSE,下面我们给出个使用 HSI 配置系统时钟例子, 起个抛砖引玉的作用。 15.3.3 硬件设计 1、RCC 2、LED 一个 RCC 是单片机内部资源,不需要外部电路。通过 LED 闪烁的频率来直观的判断不同 系统时钟频率对软件延时的效果。 15.3.4 软件设计 我们编写两个 RCC 驱动文件,bsp_clkconfig.h 和 bsp_clkconfig.c,用来存放 RCC 系统 时钟配置函数。 1. 编程要点 1、开启 HSE/HSI ,并等待 HSE/HSI 稳定 2、设置 AHB、APB2、APB1 的预分频因子 3、设置 PLL 的时钟来源,设置 VCO 输入时钟 分频因子 PLL_M,设置 VCO 输出时钟 倍频因子 PLL_N,设置 PLLCLK 时钟分频因子 PLL_P,设置 OTG FS,SDIO,RNG 时钟分频因子 PLL_Q 4、开启 PLL,并等待 PLL 稳定 5、把 PLLCK 切换为系统时钟 SYSCLK 6、读取时钟切换状态位,确保 PLLCLK 被选为系统时钟 第 130 页 共 928 零死角玩转 STM32—F429 2. 代码分析 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。 使用 HSE 配置系统时钟 代码 14 HSE 作为系统时钟来源 1 /* 2 * m: VCO 输入时钟 分频因子,取值 2~63 3 * n: VCO 输出时钟 倍频因子,取值 192~432 4 * p: SYSCLK 时钟分频因子 ,取值 2,4,6,8 5 * q: OTG FS,SDIO,RNG 时钟分频因子,取值 4~15 6 * 函数调用举例,使用 HSE 设置时钟 7 * SYSCLK=HCLK=180M,PCLK2=HCLK/2=90M,PCLK1=HCLK/4=45M 8 * HSE_SetSysClock(25, 360, 2, 7); 9 * HSE 作为时钟来源,经过 PLL 倍频作为系统时钟,这是通常的做法 10 11 * 系统时钟超频到 216M 爽一下 12 * HSE_SetSysClock(25, 432, 2, 9); 13 */ 14 void HSE_SetSysClock(uint32_t m, uint32_t n, uint32_t p, uint32_t q) 15 { 16 __IO uint32_t HSEStartUpStatus = 0; 17 18 // 使能 HSE,开启外部晶振,秉火 F429 使用 HSE=25M 19 RCC_HSEConfig(RCC_HSE_ON); 20 21 // 等待 HSE 启动稳定 22 HSEStartUpStatus = RCC_WaitForHSEStartUp(); 23 24 if (HSEStartUpStatus == SUCCESS) { 25 // 调压器电压输出级别配置为 1,以便在器件为最大频率 26 // 工作时使性能和功耗实现平衡 27 RCC->APB1ENR |= RCC_APB1ENR_PWREN; 28 PWR->CR |= PWR_CR_VOS; 29 30 // HCLK = SYSCLK / 1 31 RCC_HCLKConfig(RCC_SYSCLK_Div1); 32 33 // PCLK2 = HCLK / 2 34 RCC_PCLK2Config(RCC_HCLK_Div2); 35 36 // PCLK1 = HCLK / 4 37 RCC_PCLK1Config(RCC_HCLK_Div4); 38 39 // 如果要超频就得在这里下手啦 40 // 设置 PLL 来源时钟,设置 VCO 分频因子 m,设置 VCO 倍频因子 n, 41 // 设置系统时钟分频因子 p,设置 OTG FS,SDIO,RNG 分频因子 q 42 RCC_PLLConfig(RCC_PLLSource_HSE, m, n, p, q); 43 44 // 使能 PLL 45 RCC_PLLCmd(ENABLE); 46 47 // 等待 PLL 稳定 48 while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) { 49 } 50 51 /*-----------------------------------------------------*/ 第 131 页 共 928 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 } 零死角玩转 STM32—F429 //开启 OVER-RIDE 模式,以能达到更高频率 PWR->CR |= PWR_CR_ODEN; while ((PWR->CSR & PWR_CSR_ODRDY) == 0) { } PWR->CR |= PWR_CR_ODSWEN; while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) { } // 配置 FLASH 预取指,指令缓存,数据缓存和等待状态 FLASH->ACR = FLASH_ACR_PRFTEN | FLASH_ACR_ICEN | FLASH_ACR_DCEN | FLASH_ACR_LATENCY_5WS; /*-----------------------------------------------------*/ // 当 PLL 稳定之后,把 PLL 时钟切换为系统时钟 SYSCLK RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 读取时钟切换状态位,确保 PLLCLK 被选为系统时钟 while (RCC_GetSYSCLKSource() != 0x08) { } } else { // HSE 启动出错处理 while (1) { } } 这个函数采用库函数编写, 代码理解参考注释即可。函数有 4 个形参 m、n、p、q, 具体说明如下: 形参 形参说明 取值范围 m VCO 输入时钟 分频因子 2~63 n VCO 输出时钟 倍频因子 192~432 p PLLCLK 时钟分频因子 2/4/6/8 q OTG FS,SDIO,RNG 时钟分频因子 4~15 HSE 我们使用 25M,参数 m 我们一般也设置为 25,所以我们需要修改系统时钟的时 候只需要修改参数 n 和 p 即可,SYSCLK=PLLCLK=HSE/m*n/p。 函数调用举例:HSE_SetSysClock(25, 360, 2, 7) 把系统时钟设置为 180M,这个跟库里 面的系统时钟配置是一样的。HSE_SetSysClock(25, 432, 2, 9)把系统时钟设置为 216M,这 个是超频,要慎用。 使用 HSI 配置系统时钟 1 /* 2 * m: VCO 输入时钟 分频因子,取值 2~63 3 * n: VCO 输出时钟 倍频因子,取值 192~432 4 * p: PLLCLK 时钟分频因子 ,取值 2,4,6,8 5 * q: OTG FS,SDIO,RNG 时钟分频因子,取值 4~15 第 132 页 共 928 零死角玩转 STM32—F429 6 * 函数调用举例,使用 HSI 设置时钟 7 * SYSCLK=HCLK=180M,PCLK2=HCLK/2=90M,PCLK1=HCLK/4=45M 8 * HSI_SetSysClock(16, 360, 2, 7); 9 * HSE 作为时钟来源,经过 PLL 倍频作为系统时钟,这是通常的做法 10 11 * 系统时钟超频到 216M 爽一下 12 * HSI_SetSysClock(16, 432, 2, 9); 13 */ 14 15 void HSI_SetSysClock(uint32_t m, uint32_t n, uint32_t p, uint32_t q) 16 { 17 __IO uint32_t HSIStartUpStatus = 0; 18 19 // 把 RCC 外设初始化成复位状态 20 RCC_DeInit(); 21 22 //使能 HSI, HSI=16M 23 RCC_HSICmd(ENABLE); 24 25 // 等待 HSI 就绪 26 HSIStartUpStatus = RCC->CR & RCC_CR_HSIRDY; 27 28 // 只有 HSI 就绪之后则继续往下执行 29 if (HSIStartUpStatus == RCC_CR_HSIRDY) { 30 // 调压器电压输出级别配置为 1,以便在器件为最大频率 31 // 工作时使性能和功耗实现平衡 32 RCC->APB1ENR |= RCC_APB1ENR_PWREN; 33 PWR->CR |= PWR_CR_VOS; 34 35 // HCLK = SYSCLK / 1 36 RCC_HCLKConfig(RCC_SYSCLK_Div1); 37 38 // PCLK2 = HCLK / 2 39 RCC_PCLK2Config(RCC_HCLK_Div2); 40 41 // PCLK1 = HCLK / 4 42 RCC_PCLK1Config(RCC_HCLK_Div4); 43 44 // 如果要超频就得在这里下手啦 45 // 设置 PLL 来源时钟,设置 VCO 分频因子 m,设置 VCO 倍频因子 n, 46 // 设置系统时钟分频因子 p,设置 OTG FS,SDIO,RNG 分频因子 q 47 RCC_PLLConfig(RCC_PLLSource_HSI, m, n, p, q); 48 49 // 使能 PLL 50 RCC_PLLCmd(ENABLE); 51 52 // 等待 PLL 稳定 53 while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) { 54 } 55 56 /*-----------------------------------------------------*/ 57 //开启 OVER-RIDE 模式,以能达到更高频率 58 PWR->CR |= PWR_CR_ODEN; 59 while ((PWR->CSR & PWR_CSR_ODRDY) == 0) { 60 } 61 PWR->CR |= PWR_CR_ODSWEN; 62 while ((PWR->CSR & PWR_CSR_ODSWRDY) == 0) { 63 } 64 // 配置 FLASH 预取指,指令缓存,数据缓存和等待状态 65 FLASH->ACR = FLASH_ACR_PRFTEN 66 | FLASH_ACR_ICEN 67 |FLASH_ACR_DCEN 68 |FLASH_ACR_LATENCY_5WS; 69 /*-----------------------------------------------------*/ 70 第 133 页 共 928 71 72 73 74 75 76 77 78 79 80 81 82 } 零死角玩转 STM32—F429 // 当 PLL 稳定之后,把 PLL 时钟切换为系统时钟 SYSCLK RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); // 读取时钟切换状态位,确保 PLLCLK 被选为系统时钟 while (RCC_GetSYSCLKSource() != 0x08) { } } else { // HSI 启动出错处理 while (1) { } } 这个函数采用库函数编写, 代码理解参考注释即可。函数有 4 个形参 m、n、p、q, 具体说明如下: 形参 m n p q 形参说明 VCO 输入时钟 分频因子 VCO 输出时钟 倍频因子 PLLCLK 时钟分频因子 OTG FS,SDIO,RNG 时钟分频因子 取值范围 2~63 192~432 2/4/6/8 4~15 HSI 为 16M,参数 m 我们一般也设置为 16,所以我们需要修改系统时钟的时候只需要 修改参数 n 和 p 即可,SYSCLK=PLLCLK=HSI/m*n/p。 函数调用举例:HSI_SetSysClock(16, 360, 2, 7) 把系统时钟设置为 180M,这个跟库里 面的系统时钟配置是一样的。HSI_SetSysClock(16, 432, 2, 9)把系统时钟设置为 216M,这个 是超频,要慎用。 软件延时 1 void Delay(__IO uint32_t nCount) 2{ 3 for (; nCount != 0; nCount--); 4} 软件延时函数,使用不同的系统时钟,延时时间不一样,可以通过 LED 闪烁的频率来 判断。 MCO 输出 在 F429 中,PA8/PC9 可以复用为 MCO1/2 引脚,对外提供时钟输出,我们也可以用示 波器监控该引脚的输出来判断我们的系统时钟是否设置正确。 代码 15 MCO GPIO 初始化 1 // MCO1 PA8 GPIO 初始化 2 void MCO1_GPIO_Config(void) 3{ 4 GPIO_InitTypeDef GPIO_InitStructure; 5 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); 6 7 // MCO1 GPIO 配置 8 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; 9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 第 134 页 共 928 零死角玩转 STM32—F429 10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 11 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 12 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; 13 GPIO_Init(GPIOA, &GPIO_InitStructure); 14 } 15 16 // MCO2 PC9 GPIO 初始化 17 void MCO2_GPIO_Config(void) 18 { 19 GPIO_InitTypeDef GPIO_InitStructure; 20 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE); 21 22 // MCO2 GPIO 配置 23 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 24 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 25 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 26 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 27 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; 28 GPIO_Init(GPIOC, &GPIO_InitStructure); 29 } 秉火 F429 中 PA8 并没有引出,只引出了 PC9,如果要用示波器监控 MCO,只能用 PC9。 代码 16 MCO 输出时钟选择 1 // MCO1 输出 PLLCLK 2 RCC_MCO1Config(RCC_MCO1Source_PLLCLK, RCC_MCO1Div_1); 3 4 // MCO1 输出 SYSCLK 5 RCC_MCO2Config(RCC_MCO2Source_SYSCLK, RCC_MCO1Div_1); 我们初始化 MCO 引脚之后,可以直接调用库函数 RCC_MCOxConfig()来选择 MCO 时 钟来源,同时还可以分频,这两个参数的取值参考库函数说明即可。 主函数 在主函数中,可以调用 HSE_SetSysClock()或者 HSI_SetSysClock()这两个函数把系统 时钟设置成各种常用的时钟,然后通过 MCO 引脚监控,或者通过 LED 闪烁的快慢体验不 同的系统时钟对同一个软件延时函数的影响。 1 int main(void) 2{ 3 // 程序来到 main 函数之前,启动文件:statup_stm32f10x_hd.s 已经调用 4 // SystemInit()函数把系统时钟初始化成 72MHZ 5 // SystemInit()在 system_stm32f10x.c 中定义 6 // 如果用户想修改系统时钟,可自行编写程序修改 7 // 重新设置系统时钟,这时候可以选择使用 HSE 还是 HSI 8 9 // 使用 HSE,配置系统时钟为 180M 10 HSE_SetSysClock(25, 360, 2, 7); 11 12 //系统时钟超频到 216M 爽一下,最高是 216M,别往死里整 13 //HSE_SetSysClock(25, 432, 2, 9); 14 15 // 使用 HSI,配置系统时钟为 180M 16 //HSI_SetSysClock(16, 360, 2, 7); 17 18 // LED 端口初始化 19 LED_GPIO_Config(); 20 第 135 页 共 928 零死角玩转 STM32—F429 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 } // MCO GPIO 初始化 MCO1_GPIO_Config(); MCO2_GPIO_Config(); // MCO1 输出 PLLCLK RCC_MCO1Config(RCC_MCO1Source_PLLCLK, RCC_MCO1Div_1); // MCO2 输出 SYSCLK RCC_MCO2Config(RCC_MCO2Source_SYSCLK, RCC_MCO1Div_1); while (1) { LED1( ON ); // 亮 Delay(0x0FFFFF); LED1( OFF ); // 灭 Delay(0x0FFFFF); } 15.3.5 下载验证 把编译好的程序下载到开发板,可以看到设置不同的系统时钟时,LED 闪烁的快慢不 一样。更精确的数据我们可以用示波器监控 MCO 引脚看到。 15.4 每课一问 1、简述系统时钟的配置过程 2、F429 系统时钟最高稳定运行是多少 M 第 136 页 共 928 零死角玩转 STM32—F429 第16章 STM32 中断应用概览 本章参考资料《STM32F4xx 中文参考手册》第十章-中断和事件、《 ARM Cortex™M4F 技术参考手册》-4.3 章节:NVIC 和 4.4 章节:SCB—4.4.5 的 AIRCR。 STM32 中断非常强大,每个外设都可以产生中断,所以中断的讲解放在哪一个外设里 面去讲都不合适,这里单独抽出一章来做一个总结性的介绍,这样在其他章节涉及到中断 部分的知识我们就不用费很大的篇幅去讲解,只要示意性带过即可。 本章如无特别说明,异常就是中断,中断就是异常,请不要刻意钻牛角尖较劲。 16.1 异常类型 F429 在内核水平上搭载了一个异常响应系统, 支持为数众多的系统异常和外部中断。 其中系统异常有 10 个,外部中断有 91 个。除了个别异常的优先级被定死外,其它异常的 优先级都是可编程的。有关具体的系统异常和外部中断可在标准库文件 stm32f4xx.h 这个头 文件查询到,在 IRQn_Type 这个结构体里面包含了 F4 系列全部的异常声明。 表格 12 F429 系统异常清单 编优 号先 级 -3 -2 优先级 名称 类型 固定 固定 Reset NMI -1 固定 HardFault 0 可编程 MemManage 1 可编程 BusFault 2 可编程 UsageFault -- - 3 可编程 SVCall 4 可编程 Debug Monitor -- - 5 可编程 PendSV 6 可编程 SysTick 说明 地址 保留(实际存的是 MSP 地址) 复位 不可屏蔽中断。 RCC 时钟安全系统 (CSS) 连接到 NMI 向量 所有类型的错误 存储器管理 预取指失败,存储器访问失败 未定义的指令或非法状态 保留 通过 SWI 指令调用的系统服务 调试监控器 保留 可挂起的系统服务 系统嘀嗒定时器 0X0000 0000 0X0000 0004 0X0000 0008 0X0000 000C 0X0000 0010 0X0000 0014 0X0000 0018 0X0000 001C0X0000 002B 0X0000 002C 0X0000 0030 0X0000 0034 0X0000 0038 0X0000 003C 表格 13 F429 外部中断清单 编 优 优 先 名称 说明 地址 号 先 级类 级型 0 7 可编程 - 窗口看门狗中断 0X0000 0040 1 8 可编程 PVD 连接 EXTI 线的可编程电压检测中断 0X0000 0044 2 9 可编程 TAMP_STAMP 连接 EXTI 线的入侵和时间戳中断 0X0000 0048 第 137 页 共 928 零死角玩转 STM32—F429 中间部分省略,详情请参考 STM32F4xx 中文参考手册》第十章-中断和事件-向量表部分 84 91 可编程 SPI4 SPI4 全局中断 0X0000 0190 85 92 可编程 SPI5 SPI5 全局中断 0X0000 0194 86 93 可编程 SPI6 SPI6 全局中断 0X0000 0198 87 94 可编程 SAI1 SAI1 全局中断 0X0000 019C 88 95 可编程 LTDC LTDC 全局中断 0X0000 01A0 89 96 可编程 LTDC_ER LTDC_ER 全局中断 0X0000 01A4 90 97 可编程 DMA2D DMA2D 全局中断 0X0000 01A8 16.2 NVIC 简介 在讲如何配置中断优先级之前,我们需要先了解下 NVIC。NVIC 是嵌套向量中断控制 器,控制着整个芯片中断相关的功能,它跟内核紧密耦合,是内核里面的一个外设。但是 各个芯片厂商在设计芯片的时候会对 Cortex-M4 内核里面的 NVIC 进行裁剪,把不需要的 部分去掉,所以说 STM32 的 NVIC 是 Cortex-M4 的 NVIC 的一个子集。 16.2.1 NVIC 寄存器简介 在固件库中,NVIC 的结构体定义可谓是颇有远虑,给每个寄存器都预览了很多位, 恐怕为的是日后扩展功能。不过 STM32F429 可用不了这么多,只是用了部分而已,具体 使用了多少可参考《 ARM Cortex™-M4F 技术参考手册》-4.3.11:NVIC 寄存器映射。 代码 17 NVIC 结构体定义,来自固件库头文件:core_cm4.h 1 typedef struct { 2 __IO uint32_t ISER[8]; 3 uint32_t RESERVED0[24]; 4 __IO uint32_t ICER[8]; 5 uint32_t RSERVED1[24]; 6 __IO uint32_t ISPR[8]; 7 uint32_t RESERVED2[24]; 8 __IO uint32_t ICPR[8]; 9 uint32_t RESERVED3[24]; 10 __IO uint32_t IABR[8]; 11 uint32_t RESERVED4[56]; 12 __IO uint8_t IP[240]; 13 uint32_t RESERVED5[644]; 14 __O uint32_t STIR; 15 } NVIC_Type; // 中断使能寄存器 // 中断清除寄存器 // 中断使能悬起寄存器 // 中断清除悬起寄存器 // 中断有效位寄存器 // 中断优先级寄存器(8Bit wide) // 软件触发中断寄存器 在配置中断的时候我们一般只用 ISER、ICER 和 IP 这三个寄存器,ISER 用来使能中 断,ICER 用来失能中断,IP 用来设置中断优先级。 16.2.2 NVIC 中断配置固件库 固件库文件 core_cm4.h 的最后,还提供了 NVIC 的一些函数,这些函数遵循 CMSI 规 则,只要是 Cortex-M4 的处理器都可以使用,具体如下: 表格 14 符合 CMSIS 标准的 NVIC 库函数 NVIC 库函数 void NVIC_EnableIRQ(IRQn_Type IRQn) 描述 使能中断 第 138 页 共 928 零死角玩转 STM32—F429 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) void NVIC_SystemReset(void) 失能中断 设置中断悬起位 清除中断悬起位 获取悬起中断编号 设置中断优先级 获取中断优先级 系统复位 这些库函数我们在编程的时候用的都比较少,甚至基本都不用。在配置中断的时候我 们还有更简洁的方法,请看中断编程小节。 16.3 优先级的定义 16.3.1 优先级定义 在 NVIC 有一个专门的寄存器:中断优先级寄存器 NVIC_IPRx(在 F429 中,x=0...90) 用来配置外部中断的优先级,IPR 宽度为 8bit,原则上每个外部中断可配置的优先级为 0~255,数值越小,优先级越高。但是绝大多数 CM4 芯片都会精简设计,以致实际上支持 的优先级数减少,在 F429 中,只使用了高 4bit,如下所示: 表格 15 F429 使用 4bit 表达优先级 bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0 用于表达优先级 未使用,读回为 0 用于表达优先级的这 4bit,又被分组成抢占优先级和子优先级。如果有多个中断同时 响应,抢占优先级高的就会 抢占 抢占优先级低的优先得到执行,如果抢占优先级相同,就 比较子优先级。如果抢占优先级和子优先级都相同的话,就比较他们的硬件中断编号,编 号越小,优先级越高。 16.3.2 优先级分组 优先级的分组由内核外设 SCB 的应用程序中断及复位控制寄存器 AIRCR 的 PRIGROUP[10:8]位决定,F429 分为了 5 组,具体如下:主优先级=抢占优先级 PRIGROUP[2:0] 中断优先级值 PRI_N[7:4] 二进制点 主优先级位 子优先级位 级数 主优先级 子优先级 0b 011 0b 100 0b 101 0b 110 0b 111 0b xxxx 0b xxx.y 0b xx.yy 0b x.yyy 0b .yyyy [7:4] [7:5] [7:6] [7] None None [4] [5:4] [6:4] [7:4] 16 8 4 2 None None 2 4 9 16 设置优先级分组可调用库函数 NVIC_PriorityGroupConfig()实现,有关 NVIC 中断相 关的库函数都在库文件 misc.c 和 misc.h 中。 代码 18 中断优先级分组库函数 1 /** 2 * 配置中断优先级分组:抢占优先级和子优先级 3 * 形参如下: 第 139 页 共 928 零死角玩转 STM32—F429 4 * @arg NVIC_PriorityGroup_0: 0bit for 抢占优先级 5* 4 bits for 子优先级 6 * @arg NVIC_PriorityGroup_1: 1 bit for 抢占优先级 7* 3 bits for 子优先级 8 * @arg NVIC_PriorityGroup_2: 2 bit for 抢占优先级 9* 2 bits for 子优先级 10 * @arg NVIC_PriorityGroup_3: 3 bit for 抢占优先级 11 * 1 bits for 子优先级 12 * @arg NVIC_PriorityGroup_4: 4 bit for 抢占优先级 13 * 0 bits for 子优先级 14 * @注意 如果优先级分组为 0,则抢占优先级就不存在,优先级就全部由子优先级控制 15 */ 16 void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) 17 { 18 // 设置优先级分组 19 SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup; 20 } 表格 16 优先级分组真值表 优先级分组 NVIC_PriorityGroup_0 NVIC_PriorityGroup_1 NVIC_PriorityGroup_2 NVIC_PriorityGroup_3 NVIC_PriorityGroup_4 主优先级 0 0-1 0-3 0-7 0-15 16.4 中断编程 在配置每个中断的时候一般有 3 个编程要点: 子优先级 0-15 0-7 0-3 0-1 0 描述 主-0bit,子-4bit 主-1bit,子-3bit 主-2bit,子-2bit 主-3bit,子-1bit 主-4bit,子-0bit 1、使能外设某个中断,这个具体由每个外设的相关中断使能位控制。比如串口有发送 完成中断,接收完成中断,这两个中断都由串口控制寄存器的相关中断使能位控制。 2、初始化 NVIC_InitTypeDef 结构体,配置中断优先级分组,设置抢占优先级和子优 先级,使能中断请求。 代码 19 NVIC 初始化结构体 1 typedef struct { 2 uint8_t NVIC_IRQChannel; 3 uint8_t NVIC_IRQChannelPreemptionPriority; 4 uint8_t NVIC_IRQChannelSubPriority; 5 FunctionalState NVIC_IRQChannelCmd; 6 } NVIC_InitTypeDef; 有关 NVIC 初始化结构体的成员我们一一解释下: // 中断源 // 抢占优先级 // 子优先级 // 中断使能或者失能 1)NVIC_IROChannel:用来设置中断源,不同的中断中断源不一样,且不可写错,即 使写错了程序不会报错,只会导致不想要中断。具体的成员配置可参考 stm32f4xx.h 头文件 里面的 IRQn_Type 结构体定义,这个结构体包含了所有的中断源。 代码 20 IRQn_Type 中断源结构体 1 typedef enum IRQn { 第 140 页 共 928 零死角玩转 STM32—F429 2 //Cortex-M4 处理器异常编号 3 NonMaskableInt_IRQn = -14, 4 MemoryManagement_IRQn = -12, 5 BusFault_IRQn = -11, 6 UsageFault_IRQn = -10, 7 SVCall_IRQn = -5, 8 DebugMonitor_IRQn = -4, 9 PendSV_IRQn = -2, 10 SysTick_IRQn = -1, 11 //STM32 外部中断编号 12 WWDG_IRQn = 0, 13 PVD_IRQn = 1, 14 TAMP_STAMP_IRQn = 2, 15 16 // 限于篇幅,中间部分代码省略,具体的可查看库文件 stm32f4xx.h 17 18 SPI4_IRQn = 84, 19 SPI5_IRQn = 85, 20 SPI6_IRQn = 86, 21 SAI1_IRQn = 87, 22 LTDC_IRQn = 88, 23 LTDC_ER_IRQn = 89, 24 DMA2D_IRQn = 90 25 } IRQn_Type; 2)NVIC_IRQChannelPreemptionPriority:抢占优先级,具体的值要根据优先级分组来 确定,具体参考表格 16 优先级分组真值表 。 3)NVIC_IRQChannelSubPriority:子优先级,具体的值要根据优先级分组来确定,具 体参考表格 16 优先级分组真值表 。 4)NVIC_IRQChannelCmd:中断使能(ENABLE)或者失能(DISABLE)。操作的 是 NVIC_ISER 和 NVIC_ICER 这两个寄存器。 3、编写中断服务函数 在启动文件 startup_stm32f429_439xx.s 中我们预先为每个中断都写了一个中断服务函 数,只是这些中断函数都是为空,为的只是初始化中断向量表。实际的中断服务函数都需 要我们重新编写,中断服务函数我们统一写在 stm32f4xx_it.c 这个库文件中。 关于中断服务函数的函数名必须跟启动文件里面预先设置的一样,如果写错,系统就 在中断向量表中找不到中断服务函数的入口,直接跳转到启动文件里面预先写好的空函数, 并且在里面无限循环,实现不了中断。 16.5 每课一问 1、库文件 core_cm4.h 主要实现了什么?回去认真看库的源码 2、库文件 mics.c 和 mics.h 主要实现了什么?回去认真看库的源码 3、如果实现一次软件系统复位,具体是操作哪个寄存器的哪个位实现?答案:给内核 外设 SCB 的 AIRCR 寄存器的位 2:SYSRESETREQ 写 1 即可实现一次系统复位。 第 141 页 共 928 第17章 零死角玩转 STM32—F429 EXTI—外部中断/事件控制器 本章参考资料:《STM32F4xx 中文参考手册》系统配置控制器以及中断和事件章节。 上一章节我们已经详细介绍了 NVIC,对 STM32F4xx 中断管理系统有个全局的了解, 我们这章的内容是 NVIC 的实例应用,也是 STM32F4xx 控制器非常重要的一个资源。学习 本章时,配合《STM32F4xx 中文参考手册》系统配置控制器以及中断和事件章节一起阅读, 效果会更佳,特别是涉及到寄存器说明的部分。 特别说明,本书内容是以 STM32F42xxx 系列控制器资源讲解。 17.1 EXTI 简介 外部中断/事件控制器(EXTI)管理了控制器的 23 个中断/事件线。每个中断/事件线都对 应有一个边沿检测器,可以实现输入信号的上升沿检测和下降沿的检测。EXTI 可以实现对 每个中断/事件线进行单独配置,可以单独配置为中断或者事件,以及触发事件的属性。 17.2 EXTI 功能框图 EXTI 的功能框图包含了 EXTI 最核心内容,掌握了功能框图,对 EXTI 就有一个整体 的把握,在编程时就思路就非常清晰。EXTI 功能框图见图 17-1。 在图 17-1 可以看到很多在信号线上打一个斜杠并标注“23”字样,这个表示在控制器 内部类似的信号线路有 23 个,这与 EXTI 总共有 23 个中断/事件线是吻合的。所以我们只 要明白其中一个的原理,那其他 22 个线路原理也就知道了。 第 142 页 共 928 零死角玩转 STM32—F429 图 17-1 EXTI 功能框图 EXTI 可分为两大部分功能,一个是产生中断,另一个是产生事件,这两个功能从硬件 上就有所不同。 首先我们来看图 17-1 中红色虚线指示的电路流程。它是一个产生中断的线路,最终信 号流入到 NVIC 控制器内。 编号 1 是输入线,EXTI 控制器有 23 个中断/事件输入线,这些输入线可以通过寄存器 设置为任意一个 GPIO,也可以是一些外设的事件,这部分内容我们将在后面专门讲解。 输入线一般是存在电平变化的信号。 编号 2 是一个边沿检测电路,它会根据上升沿触发选择寄存器(EXTI_RTSR)和下降沿 触发选择寄存器(EXTI_FTSR)对应位的设置来控制信号触发。边沿检测电路以输入线作为 信号输入端,如果检测到有边沿跳变就输出有效信号 1 给编号 3 电路,否则输出无效信号 0。而 EXTI_RTSR 和 EXTI_FTSR 两个寄存器可以控制器需要检测哪些类型的电平跳变过 程,可以是只有上升沿触发、只有下降沿触发或者上升沿和下降沿都触发。 编号 3 电路实际就是一个或门电路,它一个输入来自编号 2 电路,另外一输入来自软 件中断事件寄存器(EXTI_SWIER)。EXTI_SWIER 允许我们通过程序控制就可以启动中断/ 事件线,这在某些地方非常有用。我们知道或门的作用就是有”就为 1,所以这两个输入 随便一个有有效信号 1 就可以输出 1 给编号 4 和编号 6 电路。 编号 4 电路是一个与门电路,它一个输入编号 3 电路,另外一个输入来自中断屏蔽寄 存器(EXTI_IMR)。与门电路要求输入都为 1 才输出 1,导致的结果如果 EXTI_IMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0,最终编号 4 电路输出的信号都为 0;如果 EXTI_IMR 设置为 1 时,最终编号 4 电路输出的信号才由编号 3 电路的输出信号决定,这 样我们可以简单的控制 EXTI_IMR 来实现是否产生中断的目的。编号 4 电路输出的信号会 被保存到挂起寄存器(EXTI_PR)内,如果确定编号 4 电路输出为 1 就会把 EXTI_PR 对应位 置 1。 编号 5 是将 EXTI_PR 寄存器内容输出到 NVIC 内,从而实现系统中断事件控制。 接下来我们来看看绿色虚线指示的电路流程。它是一个产生事件的线路,最终输出一 个脉冲信号。 产生事件线路是在编号 3 电路之后与中断线路有所不同,之前电路都是共用的。编号 6 电路是一个与门,它一个输入编号 3 电路,另外一个输入来自事件屏蔽寄存器 (EXTI_EMR)。如果 EXTI_EMR 设置为 0 时,那不管编号 3 电路的输出信号是 1 还是 0, 最终编号 6 电路输出的信号都为 0;如果 EXTI_EMR 设置为 1 时,最终编号 6 电路输出的 信号才由编号 3 电路的输出信号决定,这样我们可以简单的控制 EXTI_EMR 来实现是否产 生事件的目的。 编号 7 是一个脉冲发生器电路,当它的输入端,即编号 6 电路的输出端,是一个有效 信号 1 时就会产生一个脉冲;如果输入端是无效信号就不会输出脉冲。 编号 8 是一个脉冲信号,就是产生事件的线路最终的产物,这个脉冲信号可以给其他 外设电路使用,比如定时器 TIM、模拟数字转换器 ADC 等等。 第 143 页 共 928 零死角玩转 STM32—F429 产生中断线路目的是把输入信号输入到 NVIC,进一步会运行中断服务函数,实现功 能,这样是软件级的。而产生事件线路目的就是传输一个脉冲信号给其他外设使用,并且 是电路级别的信号传输,属于硬件级的。 另外,EXTI 是在 APB2 总线上的,在编程时候需要注意到这点。 17.3 中断/事件线 EXTI 有 23 个中断/事件线,每个 GPIO 都可以被设置为输入线,占用 EXTI0 至 EXTI15,还有另外七根用于特定的外设事件,见表 17-1。 七根特定外设中断/事件线由外设触发,具体用法参考《STM32F4xx 中文参考手册》 中对外设的具体说明。 表 17-1 EXTI 中断/事件线 中断/事件线 输入源 EXTI0 EXTI1 PX0(X 可为 A,B,C,D,E,F,G,H,I) PX1(X 可为 A,B,C,D,E,F,G,H,I) EXTI2 EXTI3 PX2(X 可为 A,B,C,D,E,F,G,H,I) PX3(X 可为 A,B,C,D,E,F,G,H,I) EXTI4 EXTI5 EXTI6 EXTI7 EXTI8 EXTI9 EXTI10 PX4(X 可为 A,B,C,D,E,F,G,H,I) PX5(X 可为 A,B,C,D,E,F,G,H,I) PX6(X 可为 A,B,C,D,E,F,G,H,I) PX7(X 可为 A,B,C,D,E,F,G,H,I) PX8(X 可为 A,B,C,D,E,F,G,H,I) PX9(X 可为 A,B,C,D,E,F,G,H,I) PX10(X 可为 A,B,C,D,E,F,G,H,I) EXTI11 EXTI12 EXTI13 EXTI14 EXTI15 EXTI16 EXTI17 EXTI18 EXTI19 PX11(X 可为 A,B,C,D,E,F,G,H,I) PX12(X 可为 A,B,C,D,E,F,G,H,I) PX13(X 可为 A,B,C,D,E,F,G,H,I) PX14(X 可为 A,B,C,D,E,F,G,H,I) PX15(X 可为 A,B,C,D,E,F,G,H) 可编程电压检测器(PVD)输出 RTC 闹钟事件 USB OTG FS 唤醒事件 以太网唤醒事件 EXTI20 EXTI21 EXTI22 USB OTG HS(在 FS 中配置)唤醒事件 RTC 入侵和时间戳事件 RTC 唤醒事件 EXTI0 至 EXTI15 用于 GPIO,通过编程控制可以实现任意一个 GPIO 作为 EXTI 的输 入源。由表 17-1 可知,EXTI0 可以通过 SYSCFG 外部中断配置寄存器 1(SYSCFG_EXTICR1)的 EXTI0[3:0]位选择配置为 PA0、PB0、PC0、PD0、PE0、PF0、 PG0、PH0 或者 PI0,见图 17-2。其他 EXTI 线(EXTI 中断/事件线)使用配置都是类似的。 第 144 页 共 928 零死角玩转 STM32—F429 图 17-2 EXTI0 输入源选择 17.4 EXTI 初始化结构体详解 标准库函数对每个外设都建立了一个初始化结构体,比如 EXTI_InitTypeDef,结构体 成员用于设置外设工作参数,并由外设初始化配置函数,比如 EXTI_Init()调用,这些设定 参数将会设置外设相应的寄存器,达到配置外设工作环境的目的。 初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个 成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 stm32f4xx_exti.h 文件中, 初始化库函数定义在 stm32f4xx_exti.c 文件中,编程时我们可以结合这两个文件内注释使用。 代码清单 17-1 EXTI 初始化结构体 1 typedef struct { 2 uint32_t EXTI_Line; 3 EXTIMode_TypeDef EXTI_Mode; 4 EXTITrigger_TypeDef EXTI_Trigger; 5 FunctionalState EXTI_LineCmd; 6 } EXTI_InitTypeDef; // 中断/事件线 // EXTI 模式 // 触发事件 // EXTI 控制 1) EXTI_Line:EXTI 中断/事件线选择,可选 EXTI0 至 EXTI22,可参考表 17-1 选择。 2) EXTI_Mode:EXTI 模式选择,可选为产生中断(EXTI_Mode_Interrupt)或者产生事 件(EXTI_Mode_Event)。 3) EXTI_Trigger:EXTI 边沿触发事件,可选上升沿触发(EXTI_Trigger_Rising)、下 降 沿 触 发 ( EXTI_Trigger_Falling) 或 者 上 升 沿 和 下 降 沿 都 触 发 ( EXTI_Trigger_Rising_Falling)。 4) EXTI_LineCmd:控制是否使能 EXTI 线,可选使能 EXTI 线(ENABLE)或禁用 (DISABLE)。 17.5 外部中断控制实验 中断在嵌入式应用中占有非常重要的地位,几乎每个控制器都有中断功能。中断对保 证紧急事件得到第一时间处理是非常重要的 第 145 页 共 928 零死角玩转 STM32—F429 我们设计使用外接的按键来作为触发源,使得控制器产生中断,并在中断服务函数中 实现控制 RGB 彩灯的任务。 17.5.1 硬件设计 轻触按键在按下时会使得引脚接通,通过电路设计可以使得按下时产生电平变化,见 图 17-1。 图 17-3 按键电路设计 17.5.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_exti.c 和 bsp_exti.h 文件用来存 放 EXTI 驱动程序及相关宏定义,中断服务函数放在 stm32f4xx_it.h 文件中。 1. 编程要点 1) 初始化 RGB 彩灯的 GPIO; 2) 开启按键 GPIO 时钟和 SYSCFG 时钟; 3) 配置 NVIC; 4) 配置按键 GPIO 为输入模式; 5) 将按键 GPIO 连接到 EXTI 源输入; 6) 配置按键 EXTI 中断/事件线; 7) 编写 EXTI 中断服务函数。 第 146 页 共 928 零死角玩转 STM32—F429 2. 软件分析 按键和 EXTI 宏定义 代码清单 17-2 按键和 EXTI 宏定义 1 //引脚定义 2 /*******************************************************/ 3 #define KEY1_INT_GPIO_PORT GPIOA 4 #define KEY1_INT_GPIO_CLK RCC_AHB1Periph_GPIOA 5 #define KEY1_INT_GPIO_PIN GPIO_Pin_0 6 #define KEY1_INT_EXTI_PORTSOURCE EXTI_PortSourceGPIOA 7 #define KEY1_INT_EXTI_PINSOURCE EXTI_PinSource0 8 #define KEY1_INT_EXTI_LINE EXTI_Line0 9 #define KEY1_INT_EXTI_IRQ EXTI0_IRQn 10 11 #define KEY1_IRQHandler EXTI0_IRQHandler 12 13 #define KEY2_INT_GPIO_PORT GPIOC 14 #define KEY2_INT_GPIO_CLK RCC_AHB1Periph_GPIOC 15 #define KEY2_INT_GPIO_PIN GPIO_Pin_13 16 #define KEY2_INT_EXTI_PORTSOURCE EXTI_PortSourceGPIOC 17 #define KEY2_INT_EXTI_PINSOURCE EXTI_PinSource13 18 #define KEY2_INT_EXTI_LINE EXTI_Line13 19 #define KEY2_INT_EXTI_IRQ EXTI15_10_IRQn 20 21 #define KEY2_IRQHandler EXTI15_10_IRQHandler 使用宏定义方法指定与电路设计相关配置,这对于程序移植或升级非常有用的。 嵌套向量中断控制器 NVIC 配置 代码清单 17-3 NVIC 配置 1 static void NVIC_Configuration(void) 2{ 3 NVIC_InitTypeDef NVIC_InitStructure; 4 5 /* 配置 NVIC 为优先级组 1 */ 6 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); 7 8 /* 配置中断源:按键 1 */ 9 NVIC_InitStructure.NVIC_IRQChannel = KEY1_INT_EXTI_IRQ; 10 /* 配置抢占优先级:1 */ 11 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 12 /* 配置子优先级:1 */ 13 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 14 /* 使能中断通道 */ 15 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 16 NVIC_Init(&NVIC_InitStructure); 17 18 /* 配置中断源:按键 2,其他使用上面相关配置 */ 19 NVIC_InitStructure.NVIC_IRQChannel = KEY2_INT_EXTI_IRQ; 20 NVIC_Init(&NVIC_InitStructure); 21 } 有关 NVIC 配置问题可参考上一章节内容,这里不做过多解释。 EXTI 中断配置 代码清单 17-4 EXTI 中断配置 第 147 页 共 928 零死角玩转 STM32—F429 1 void EXTI_Key_Config(void) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 EXTI_InitTypeDef EXTI_InitStructure; 5 6 /*开启按键 GPIO 口的时钟*/ 7 RCC_AHB1PeriphClockCmd(KEY1_INT_GPIO_CLK|KEY2_INT_GPIO_CLK ,ENABLE); 8 9 /* 使能 SYSCFG 时钟 ,使用 GPIO 外部中断时必须使能 SYSCFG 时钟*/ 10 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); 11 12 /* 配置 NVIC */ 13 NVIC_Configuration(); 14 15 /* 选择按键 1 的引脚 */ 16 GPIO_InitStructure.GPIO_Pin = KEY1_INT_GPIO_PIN; 17 /* 设置引脚为输入模式 */ 18 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; 19 /* 设置引脚不上拉也不下拉 */ 20 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 21 /* 使用上面的结构体初始化按键 */ 22 GPIO_Init(KEY1_INT_GPIO_PORT, &GPIO_InitStructure); 23 24 /* 连接 EXTI 中断源 到 key1 引脚 */ 25 SYSCFG_EXTILineConfig(KEY1_INT_EXTI_PORTSOURCE, 26 KEY1_INT_EXTI_PINSOURCE); 27 28 /* 选择 EXTI 中断源 */ 29 EXTI_InitStructure.EXTI_Line = KEY1_INT_EXTI_LINE; 30 /* 中断模式 */ 31 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; 32 /* 下降沿触发 */ 33 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; 34 /* 使能中断/事件线 */ 35 EXTI_InitStructure.EXTI_LineCmd = ENABLE; 36 EXTI_Init(&EXTI_InitStructure); 37 38 /* 选择按键 2 的引脚 */ 39 GPIO_InitStructure.GPIO_Pin = KEY2_INT_GPIO_PIN; 40 /* 其他配置与上面相同 */ 41 GPIO_Init(KEY2_INT_GPIO_PORT, &GPIO_InitStructure); 42 43 /* 连接 EXTI 中断源 到 key2 引脚 */ 44 SYSCFG_EXTILineConfig(KEY2_INT_EXTI_PORTSOURCE, 45 KEY2_INT_EXTI_PINSOURCE); 46 47 /* 选择 EXTI 中断源 */ 48 EXTI_InitStructure.EXTI_Line = KEY2_INT_EXTI_LINE; 49 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; 50 /* 上升沿触发 */ 51 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; 52 EXTI_InitStructure.EXTI_LineCmd = ENABLE; 53 EXTI_Init(&EXTI_InitStructure); 54 } 首先,使用 GPIO_InitTypeDef 和 EXTI_InitTypeDef 结构体定义两个用于 GPIO 和 EXTI 初始化配置的变量,关于这两个结构体前面都已经做了详细的讲解。 使用 GPIO 之前必须开启 GPIO 端口的时钟;用到 EXTI 必须开启 SYSCFG 时钟。 调用 NVIC_Configuration 函数完成对按键 1、按键 2 优先级配置并使能中断通道。 作为中断/时间输入线把 GPIO 配置为输入模式,这里不使用上拉或下拉,有外部电路 完全决定引脚的状态。 第 148 页 共 928 零死角玩转 STM32—F429 SYSCFG_EXTILineConfig 函数用来指定中断/事件线的输入源,它实际是设定 SYSCFG 外部中断配置寄存器的值,该函数接收两个参数,第一个参数指定 GPIO 端口源, 第二个参数为选择对应 GPIO 引脚源编号。 我们的目的是产生中断,执行中断服务函数,EXTI 选择中断模式,按键 1 使用下降沿 触发方式,并使能 EXTI 线。 按键 2 基本上采用与按键 1 相关参数配置,只是改为上升沿触发方式。 EXTI 中断服务函数 代码清单 17-5 EXTI 中断服务函数 1 void KEY1_IRQHandler(void) 2{ 3 //确保是否产生了 EXTI Line 中断 4 if (EXTI_GetITStatus(KEY1_INT_EXTI_LINE) != RESET) { 5 // LED1 取反 6 LED1_TOGGLE; 7 //清除中断标志位 8 EXTI_ClearITPendingBit(KEY1_INT_EXTI_LINE); 9 } 10 } 11 12 void KEY2_IRQHandler(void) 13 { 14 //确保是否产生了 EXTI Line 中断 15 if (EXTI_GetITStatus(KEY2_INT_EXTI_LINE) != RESET) { 16 // LED2 取反 17 LED2_TOGGLE; 18 //清除中断标志位 19 EXTI_ClearITPendingBit(KEY2_INT_EXTI_LINE); 20 } 21 } 当中断发生时,对应的中断服务函数就会被执行,我们可以在中断服务函数实现一些 控制。 一般为确保中断确实发生,我们会在中断服务函数调用中断标志位状态读取函数读取 外设中断标志位并判断标志位状态。 EXTI_GetITStatus 函数用来获取 EXTI 的中断标志位状态,如果 EXTI 线有中断发生函 数返回“SET”否则返回“RESET”。实际上,EXTI_GetITStatus 函数是通过读取 EXTI_PR 寄存器值来判断 EXTI 线状态的。 按键 1 的中断服务函数我们让 LED1 翻转其状态,按键 2 的中断服务函数我们让 LED2 翻转其状态。执行任务后需要调用 EXTI_ClearITPendingBit 函数清除 EXTI 线的中断标志 位。 主函数 代码清单 17-6 主函数 1 int main(void) 2{ 3 /* LED 端口初始化 */ 4 LED_GPIO_Config(); 5 第 149 页 共 928 零死角玩转 STM32—F429 6 7 8 9 10 11 12 13 14 15 } /* 初始化 EXTI 中断,按下按键会触发中断, * 触发中断会进入 stm32f4xx_it.c 文件中的函数 * KEY1_IRQHandler 和 KEY2_IRQHandler,处理中断,反转 LED 灯。 */ EXTI_Key_Config(); /* 等待中断,由于使用中断方式,CPU 不用轮询按键 */ while (1) { } 主函数非常简单,只有两个任务函数。LED_GPIO_Config 函数定义在 bsp_led.c 文件内, 完成 RGB 彩灯的 GPIO 初始化配置。EXTI_Key_Config 函数完成两个按键的 GPIO 和 EXTI 配置。 17.5.3 下载验证 保证开发板相关硬件连接正确,把编译好的程序下载到开发板。此时 RGB 彩色灯是暗 的,如果我们按下开发板上的按键 1,RGB 彩灯变亮,再按下按键 1,RGB 彩灯又变暗; 如果我们按下开发板上的按键 2 并弹开,RGB 彩灯变亮,再按下开发板上的 KEY2 并弹开, RGB 彩灯又变暗。 每课一问 1、 是否可以同时使用 PA0 和 PB0 中断?如果不可以,有什么解决方法。 2、 从硬件角度结合程序分析,为什么按下按键 1RGB 彩灯就马上变化,而按键 2 却 需要按下按键再弹开之后 RGB 彩灯才变化? 第 150 页 共 928 零死角玩转 STM32—F429 第18章 SysTick—系统定时器 本章参考资料《 ARM Cortex™-M4F 技术参考手册》-4.5 章节 SysTick Timer(STK),和 4.48 章节 SHPRx,其中 STK 这个章节有 SysTick 的简介和寄存器的详细描述。因为 SysTick 是属于 CM4 内核的外设,有关寄存器的定义和部分库函数都在 core_cm4.h 这个头 文件中实现。所以学习 SysTick 的时候可以参考这两个资料,一个是文档,一个是源码。 18.1 SysTick 简介 SysTick—系统定时器是属于 CM4 内核中的一个外设,内嵌在 NVIC 中。系统定时器 是一个 24bit 的向下递减的计数器,计数器每计数一次的时间为 1/SYSCLK,一般我们设置 系统时钟 SYSCLK 等于 180M。当重装载数值寄存器的值递减到 0 的时候,系统定时器就 产生一次中断,以此循环往复。 因为 SysTick 是属于 CM4 内核的外设,所以所有基于 CM4 内核的单片机都具有这个 系统定时器,使得软件在 CM4 单片机中可以很容易的移植。系统定时器一般用于操作系统, 用于产生时基,维持操作系统的心跳。 18.2 SysTick 寄存器介绍 SysTick—系统定时有 4 个寄存器,简要介绍如下。在使用 SysTick 产生定时的时候, 只需要配置前三个寄存器,最后一个校准寄存器不需要使用。 表 18-1 SysTick 寄存器汇总 寄存器名称 寄存器描述 CTRL LOAD SysTick 控制及状态寄存器 SysTick 重装载数值寄存器 VAL CALIB SysTick 当前数值寄存器 SysTick 校准数值寄存器 表 18-2 SysTick 控制及状态寄存器 位段 名称 类型 复位值 描述 16 COUNTFLAG R/W 0 2 CLKSOURCE R/W 0 1 TICKINT R/W 0 如果在上次读取本寄存器后, SysTick 已经计到 了 0,则该位为 1。 时钟源选择位,0=AHB/8,1=处理器时钟 AHB 1=SysTick 倒数计数到 0 时产生 SysTick 异常请 求 , 0= 数 到 0 时 无 动 作 。 也 可 以 通 过 读 取 COUNTFLAG 标志位来确定计数器是否递减到 0 ENABLE R/W 0 0 SysTick 定时器的使能位 表 18-3 SysTick 重装载数值寄存器 位段 名称 类型 复位值 描述 23:0 RELOAD R/W 0 当倒数计数至零时,将被重装载的值 表 18-4 SysTick 当前数值寄存器 位段 名称 类型 复位值 23:0 CURRENT R/W 0 描述 读取时返回当前倒计数的值,写它则使之清 第 151 页 共 928 零死角玩转 STM32—F429 零,同时还会清除在 SysTick 控制及状态寄 存器中的 COUNTFLAG 标志 表 18-5 SysTick 校准数值寄存器 位段 名称 类型 复位值 描述 31 NOREF R 0 NOREF flag. Reads as zero. Indicates that a separate reference clock is provided. The frequency of this clock is HCLK/8 30 SKEW R 1 SKEW flag: Indicates whether the TENMS value is exact. Reads as one. Calibration value for the 1 ms inexact timing is not known because TENMS is not known. This can affect the suitability of SysTick as a software real time clock 23:0 TENMS R 0 Calibration value. Indicates the calibration value when the SysTick counterruns on HCLK max/8 as external clock. The value is product dependent, please refer to theProduct Reference Manual, SysTick Calibration Value section. When HCLK is programmed atthe maximum frequency, the SysTick period is 1ms. If calibration information is not known, calculate the calibration value required from thefrequency of the processor clock or external clock 系统定时器的校准数值寄存器在定时实验中不需要用到。有关各个位的描述这里引用 手册里面的英文版本,比较晦涩难懂,暂时不知道这个寄存器用来干什么。有研究过的朋 友可以交流,起个抛砖引玉的作用。 18.3 SysTick 定时实验 利用 SysTick 产生 1s 的时基,LED 以 1s 的频率闪烁。 18.3.1 硬件设计 SysTick 属于单片机内部的外设,不需要额外的硬件电路,剩下的只需一个 LED 灯即 可。 18.3.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_SysTick.c 和 bsp_ SysTick.h 文 件用来存放 SysTick 驱动程序及相关宏定义,中断服务函数放在 stm32f4xx_it.h 文件中。 1. 编程要点 1、设置重装载寄存器的值 2、清除当前数值寄存器的值 3、配置控制与状态寄存器 第 152 页 共 928 零死角玩转 STM32—F429 2. 代码分析 SysTick 属于内核的外设,有关的寄存器定义和库函数都在内核相关的库文件 core_cm4.h 中。 SysTick 配置库函数 代码 18-1SysTick 配置库函数 1 __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks) 2{ 3 // 不可能的重装载值,超出范围 4 if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk) { 5 return (1UL); 6 } 7 8 // 设置重装载寄存器 9 SysTick->LOAD = (uint32_t)(ticks - 1UL); 10 11 // 设置中断优先级 12 NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); 13 14 // 设置当前数值寄存器 15 SysTick->VAL = 0UL; 16 17 // 设置系统定时器的时钟源为 AHBCLK=180M 18 // 使能系统定时器中断 19 // 使能定时器 20 SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | 21 SysTick_CTRL_TICKINT_Msk | 22 SysTick_CTRL_ENABLE_Msk; 23 return (0UL); 24 } 用固件库编程的时候我们只需要调用库函数 SysTick_Config()即可,形参 ticks 用来设 置重装载寄存器的值,最大不能超过重装载寄存器的值 224,当重装载寄存器的值递减到 0 的时候产生中断,然后重装载寄存器的值又重新装载往下递减计数,以此循环往复。紧随 其后设置好中断优先级,最后配置系统定时器的时钟为 180M,使能定时器和定时器中断, 这样系统定时器就配置好了,一个库函数搞定。 SysTick_Config()库函数主要配置了 SysTick 中的三个寄存器:LOAD、VAL 和 CTRL, 有关具体的部分看代码注释即可。其中还调用了固件库函数 NVIC_SetPriority()来配置系统 定时器的中断优先级,该库函数也在 core_m4.h 中定义,原型如下: 1 __STATIC_INLINE void NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority) 2{ 3 if ((int32_t)IRQn < 0) { 4 SCB->SHP[(((uint32_t)(int32_t)IRQn) & 0xFUL)-4UL] = 5 (uint8_t)((priority << (8 - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL); 6 } else { 7 NVIC->IP[((uint32_t)(int32_t)IRQn)] = 8 (uint8_t)((priority << (8 - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL); 9} 10 } 因为 SysTick 属于内核外设,跟普通外设的中断优先级有些区别,并没有抢占优先级 和子优先级的说法。在 STM32F429 中,内核外设的中断优先级由内核 SCB 这个外设的寄 存器:SHPRx(x=1.2.3)来配置。有关 SHPRx 寄存器的详细描述可参考《Cortex-M4 内核 编程手册》4.4.8 章节。下面我们简单介绍下这个寄存器。 第 153 页 共 928 零死角玩转 STM32—F429 SPRH1-SPRH3 是一个 32 位的寄存器,但是只能通过字节访问,每 8 个字段控制着一 个内核外设的中断优先级的配置。在 STM32F429 中,只有位 7:3 这高四位有效,低四位没 有用到,所以内核外设的中断优先级可编程为:0~15,只有 16 个可编程优先级,数值越小, 优先级越高。如果软件优先级配置相同,那就根据他们在中断向量表里面的位置编号来决 定优先级大小,编号越小,优先级越高。 表 18-6 系统异常优先级字段 异常 字段 寄存器描述 Memory management fault Bus fault PRI_4 PRI_5 SHPR1 Usage fault PRI_6 SVCall PRI_11 SHPR2 PendSV SysTick PRI_14 PRI_15 SHPR3 如果要修改内核外设的优先级,只需要修改下面三个寄存器对应的某个字段即可。 图 18-1 SHPR1 寄存器 图 18-2 SHPR2 寄存器 第 154 页 共 928 零死角玩转 STM32—F429 图 18-3 SHPR3 寄存器 在系统定时器中,配置优先级为(1UL << __NVIC_PRIO_BITS) - 1UL),其中宏 __NVIC_PRIO_BITS 为 4,那计算结果就等于 15,可以看出系统定时器此时设置的优先级 在内核外设中是最低的。 1 // 设置系统定时器中断优先级 2 NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); SysTick 初始化函数 代码 18-2 SysTick 初始化函数 1 /** 2 * @brief 启动系统滴答定时器 SysTick 3 * @param 无 4 * @retval 无 5 */ 6 void SysTick_Init(void) 7{ 8 /* SystemFrequency / 1000 1ms 中断一次 9 * SystemFrequency / 100000 10us 中断一次 10 * SystemFrequency / 1000000 1us 中断一次 11 */ 12 if (SysTick_Config(SystemCoreClock / 100000)) { 13 /* Capture error */ 14 while (1); 15 } 16 } SysTick 初始化函数由用户编写,里面调用了 SysTick_Config()这个固件库函数,通过 设置该固件库函数的形参,就决定了系统定时器经过多少时间就产生一次中断。 SysTick 中断时间的计算 SysTick 定时器的计数器是向下递减计数的,计数一次的时间 TDEC=1/CLKAHB,当重装 载寄存器中的值 VALUELOAD 减到 0 的时候,产生中断,可知中断一次的时间 TINT=VALUELOAD * TDEC 中断= VALUELOAD/CLKAHB,其中 CLKAHB =180MHZ。如果设置为 180,那中断一次的时间 TINT=180/180M=1us。不过 1us 的中断没啥意义,整个程序的重心 都花在进出中断上了,根本没有时间处理其他的任务。 SysTick_Config(SystemCoreClock / 100000)) SysTick_Config()的形我们配置为 SystemCoreClock / 100000=180M/100000=1800, 从刚刚分析我们知道这个形参的值最终是写到重装载寄存器 LOAD 中的,从而可知我们现 在把 SysTick 定时器中断一次的时间 TINT=1800/180M=10us。 第 155 页 共 928 零死角玩转 STM32—F429 SysTick 定时时间的计算 当设置好中断时间 TINT 后,我们可以设置一个变量 t,用来记录进入中断的次数,那 么变量 t 乘以中断的时间 TINT 就可以计算出需要定时的时间。 SysTick 定时函数 现在我们定义一个微秒级别的延时函数,形参为 nTime,当用这个形参乘以中断时间 TINT 就得出我们需要的延时时间,其中 TINT 我们已经设置好为 10us。关于这个函数的具体 调用看注释即可。 1 /** 2 * @brief us 延时程序,10us 为一个单位 3 * @param 4 * @arg nTime: Delay_us( 1 ) 则实现的延时为 1 * 10us = 10us 5 * @retval 无 6 */ 7 void Delay_us(__IO u32 nTime) 8{ 9 TimingDelay = nTime; 10 11 while (TimingDelay != 0); 12 } 函数 Delay_us()中我们等待 TimingDelay 为 0,当 TimingDelay 为 0 的时候表示延时时 间到。变量 TimingDelay 在中断函数中递减,即 SysTick 每进一次中断即 10us 的时间 TimingDelay 递减一次。 SysTick 中断服务函数 1 void SysTick_Handler(void) 2{ 3 TimingDelay_Decrement(); 4} 中断复位函数调用了另外一个函数 TimingDelay_Decrement(),原型如下: 1 /** 2 * @brief 获取节拍程序 3 * @param 无 4 * @retval 无 5 * @attention 在 SysTick 中断函数 SysTick_Handler()调用 6 */ 7 void TimingDelay_Decrement(void) 8{ 9 if (TimingDelay != 0x00) { 10 TimingDelay--; 11 } 12 } TimingDelay 的值等于延时函数中传进去的 nTime 的值,比如 nTime=100000,则延时 的时间等于 100000*10us=1s。 主函数 1 int main(void) 2{ 3 /* LED 端口初始化 */ 4 LED_GPIO_Config(); 5 6 /* 配置 SysTick 为 10us 中断一次,时间到后触发定时中断, 7 *进入 stm32fxx_it.c 文件的 SysTick_Handler 处理,通过数中断次数计时 8 */ 第 156 页 共 928 零死角玩转 STM32—F429 9 10 11 12 13 14 15 16 17 18 19 20 21 22 } SysTick_Init(); while (1) { LED_RED; Delay_us(100000); LED_GREEN; Delay_us(100000); LED_BLUE; Delay_us(100000); } // 10000 * 10us = 1000ms // 10000 * 10us = 1000ms // 10000 * 10us = 1000ms 主函数中初始化了 LED 和 SysTick,然后在一个 while 循环中以 1s 的频率让 LED 闪烁。 18.4 每课一问 1、如果修改 SysTick 的中断优先级? 2、如何计算 SysTick 进入一次中断的时间? 3、如何利用 SysTick 实现一个 1ms 的延时? 第 157 页 共 928 零死角玩转 STM32—F429 第19章 通讯的基本概念 在计算机设备与设备之间或集成电路之间常常需要进行数据传输,在本书后面的章节 中我们会学习到各种各样的通讯方式,所以在本章中我们先统一介绍这些通讯的基本概念。 19.1 串行通讯与并行通讯 按数据传送的方式,通讯可分为串行通讯与并行通讯,串行通讯是指设备之间通过少 量数据信号线(一般是 8 根以下),地线以及控制信号线,按数据位形式一位一位地传输数 据的通讯方式。而并行通讯一般是指使用 8、16、32 及 64 根或更多的数据线进行传输的通 讯方式,它们的通讯传输对比说明见图 19-1,并行通讯就像多个车道的公路,可以同时传 输多个数据位的数据,而串行通讯,而串行通讯就像单个车道的公路,同一时刻只能传输 一个数据位的数据。 图 19-1 并行通讯与串行通讯的对比图 第 158 页 共 928 零死角玩转 STM32—F429 很明显,因为一次可传输多个数据位的数据 ,在数据传输速率相同的情况下,并行通 讯传输的数据量要大得多,而串行通讯则可以节省数据线的硬件成本(特别是远距离时)以 及 PCB 的布线面积,串行通讯与并行通讯的特性对比见表 19-1。 表 19-1 串行通讯与并行通讯的特性对比 特性 通讯距离 抗干扰能力 传输速率 成本 串行通讯 较远 较强 较慢 较低 并行通讯 较近 较弱 较高 较高 不过由于并行传输对同步要求较高,且随着通讯速率的提高,信号干扰的问题会显著 影响通讯性能,现在随着技术的发展,越来越多的应用场合采用高速率的串行差分传输。 19.2 全双工、半双工及单工通讯 根据数据通讯的方向,通讯又分为全双工、半双工及单工通讯,它们主要以信道的方 向来区分,见图 19-2 及表 19-2。 表 19-2 通讯方式说明 通讯方式 全双工 半双工 单工 说明 在同一时刻,两个设备之间可以同时收发数据 两个设备之间可以收发数据,但不能在同一时刻进行 在任何时刻都只能进行一个方向的通讯,即一个固定为发送设备,另一个 固定为接收设备 仍以公路来类比,全双工的通讯就是一个双向车道,两个方向上的车流互不相干;半 双工则像乡间小道那样,同一时刻只能让一辆小车通过,另一方向的来车只能等待道路空 出来时才能经过;而单工则像单行道,另一方向的车辆完全禁止通行。 第 159 页 共 928 零死角玩转 STM32—F429 图 19-2 全双工、半双工及单工通讯 19.3 同步通讯与异步通讯 根据通讯的数据同步方式,又分为同步和异步两种,可以根据通讯过程中是否有使用 到时钟信号进行简单的区分。 在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下 双方进行协调,同步数据,见图 19-3。通讯中通常双方会统一规定在时钟信号的上升沿或 下降沿对数据线进行采样。 第 160 页 共 928 零死角玩转 STM32—F429 图 19-3 同步通讯 在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用 的信号位,或者把主体数据进行打包,以数据帧的格式传输数据,见图 19-4,某些通讯中 还需要双方约定数据的传输速率,以便更好地同步。 图 19-4 某种异步通讯 在同步通讯中,数据信号所传输的内容绝大部分就是有效数据,而异步通讯中会包含 有帧的各种标识符,所以同步通讯的效率更高,但是同步通讯双方的时钟允许误差较小, 而异步通讯双方的时钟允许误差较大。 第 161 页 共 928 零死角玩转 STM32—F429 19.4 通讯速率 衡量通讯性能的一个非常重要的参数就是通讯速率,通常以比特率(Bitrate)来表示,即 每秒钟传输的二进制位数,单位为比特每秒(bit/s)。容易与比特率混淆的概念是“波特率” (Baudrate),它表示每秒钟传输了多少个码元。而码元是通讯信号调制的概念,通讯中常用 时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。如常见的通讯传输中, 用 0V 表示数字 0,5V 表示数字 1,那么一个码元可以表示两种状态 0 和 1,所以一个码元 等于一个二进制比特位,此时波特率的大小与比特率一致;如果在通讯传输中,有 0V、 2V、4V 以及 6V 分别表示二进制数 00、01、10、11,那么每个码元可以表示四种状态, 即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率 的一半。因为很多常见的通讯中一个码元都是表示两种状态,人们常常直接以波特率来表 示比特率,虽然严格来说没什么错误,但希望您能了解它们的区别。 第 162 页 共 928 零死角玩转 STM32—F429 第20章 USART—串口通讯 本章参考资料:《STM32F4xx 中文参考手册》USART 章节。 学习本章时,配合《STM32F4xx 中文参考手册》USART 章节一起阅读,效果会更佳, 特别是涉及到寄存器说明的部分。 特别说明,本书内容是以 STM32F42xxx 系列控制器资源讲解。 20.1 串口通讯协议简介 串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式,因为它简单 便捷,大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通讯方 式输出调试信息。 在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和 片上外设;STM32 标准库则是在寄存器与用户代码之间的软件层。对于通讯协议,我们也 以分层的方式来理解,最基本的是把它分为物理层和协议层。物理层规定通讯系统中具有 机械、电子功能部分的特性,确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑, 统一收发双方的数据打包、解包标准。简单来说物理层规定我们用嘴巴还是用肢体来交流, 协议层则规定我们用中文还是英文来交流。 下面我们分别对串口通讯协议的物理层及协议层进行讲解。 20.1.1 物理层 串口通讯的物理层有很多标准及变种,我们主要讲解 RS-232 标准 ,RS-232 标准主要 规定了信号的用途、通讯接口以及信号的电平标准。 使用 RS-232 标准的串口设备间常见的通讯结构见图 20-1。 图 20-1 串口通讯结构图 在上面的通讯方式中,两个通讯设备的“DB9 接口”之间通过串口信号线建立起连接, 串口信号线中使用“RS-232 标准”传输数据信号。由于 RS-232 电平标准的信号不能直接 被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的 “TTL 校准”的电平信号,才能实现通讯。 第 163 页 共 928 零死角玩转 STM32—F429 1. 电平标准 根据通讯使用的电平标准不同,串口通讯可分为 TTL 标准及 RS-232 标准,见表 20-1。 表 20-1 TTL 电平标准与 RS232 电平标准 通讯标准 5V TTL RS-232 电平标准(发送端) 逻辑 1:2.4V-5V 逻辑 0:0~0.5V 逻辑 1:-15V~-3V 逻辑 0:+3V~+15V 我们知道常见的电子电路中常使用 TTL 的电平标准,理想状态下,使用 5V 表示二进 制逻辑 1,使用 0V 表示逻辑 0;而为了增加串口通讯的远距离传输及抗干扰能力,它使用- 15V 表示逻辑 1,+15V 表示逻辑 0。使用 RS232 与 TTL 电平校准表示同一个信号时的对比 见图 20-2。 图 20-2 RS-232 与 TTL 电平标准下表示同一个信号 因为控制器一般使用 TTL 电平标准,所以常常会使用 MA3232 芯片对 TTL 及 RS-232 电平的信号进行互相转换。 2. RS-232 信号线 在最初的应用中,RS-232 串口标准常用于计算机、路由与调制调解器(MODEN,俗称 “猫”)之间的通讯 ,在这种通讯系统中,设备被分为数据终端设备 DTE(计算机、路由)和 数据通讯设备 DCE(调制调解器)。我们以这种通讯模型讲解它们的信号线连接方式及各个 信号线的作用。 在旧式的台式计算机中一般会有 RS-232 标准的 COM 口(也称 DB9 接口),见图 20-3。 第 164 页 共 928 零死角玩转 STM32—F429 图 20-3 电脑主板上的 COM 口及串口线 其中接线口以针式引出信号线的称为公头,以孔式引出信号线的称为母头。在计算机 中一般引出公头接口,而在调制调解器设备中引出的一般为母头,使用上图中的串口线即 可把它与计算机连接起来。通讯时,串口线中传输的信号就是使用前面讲解的 RS-232 标准 调制的。 在这种应用场合下,DB9 接口中的公头及母头的各个引脚的标准信号线接法见图 20-4 及表 20-2。 图 20-4 DB9 标准的公头及母头接法 表 20-2 DB9 信号线说明(公头,为方便理解,可把 DTE 理解为计算机,DCE 理解为调制调 第 165 页 共 928 零死角玩转 STM32—F429 解器) 序号 名称 符号 1 载波检测 DCD 2 接收数据 RXD 3 发送数据 TXD 4 数 据 终 端 DTR (DTE) 就 绪 5 信号地 GND 6 数 据 设 备 DSR (DCE) 就 绪 7 请求发送 RTS 8 允许发送 CTS 9 响铃指示 RI 数据方向 DTEDCE DTEDCE DTEDCE DTEDCE 说明 Data Carrier Detect , 数 据 载 波 检 测 , 用 于 DTE 告知对方,本机是否收到对方的载波信 号 Receive Data,数据接收信号,即输入 。 Transmit Data,数据发送信号,即输出。两个 设备之间的 TXD 与 RXD 应交叉相连 Data Terminal Ready,数据终端就绪,用于 DTE 向对方告知本机是否已准备好 DTEDCE 地线,两个通讯设备之间的地电位可能不一 样,这会影响收发双方的电平信号,所以两 个串口设备之间必须要使用地线连接,即共 地。 Data Set Ready,数据发送就绪,用于 DCE 告 知对方本机是否处于待命状态 DTEDCE DTEDCE DTEDCE Request To Send,请求发送, DTE 请求 DCE 本设备向 DCE 端发送数据 Clear To Send,允许发送,DCE 回应对方的 RTS 发送请求,告知对方是否可以发送数据 Ring Indicator,响铃指示,表示 DCE 端与线 路已接通 上表中的是计算机端的 DB9 公头标准接法,由于两个通讯设备之间的收发信号(RXD 与 TXD)应交叉相连,所以调制调解器端的 DB9 母头的收发信号接法一般与公头的相反, 两个设备之间连接时,只要使用“直通型”的串口线连接起来即可,见图 20-5。 图 20-5 计算机与调制调解器的信号线连接 串口线中的 RTS、CTS、DSR、DTR 及 DCD 信号,使用逻辑 1 表示信号有效,逻辑 0 表示信号无效。例如,当计算机端控制 DTR 信号线表示为逻辑 1 时,它是为了告知远端的 调制调解器,本机已准备好接收数据,0 则表示还没准备就绪。 在目前的其它工业控制使用的串口通讯中,一般只使用 RXD、TXD 以及 GND 三条信 号线,直接传输数据信号。而 RTS、CTS、DSR、DTR 及 DCD 信号都被裁剪掉了,如果 您在前面被这些信号弄得晕头转向,那就直接忽略它们吧。 第 166 页 共 928 零死角玩转 STM32—F429 20.1.2 协议层 串口通讯的数据包由发送设备通过自身的 TXD 接口传输到接收设备的 RXD 接口。在 串口通讯的协议层中,规定了数据包的内容,它由启始位、主体数据、校验位以及停止位 组成,通讯双方的数据包格式要约定一致才能正常收发数据,其组成见图 20-6。 图 20-6 串口数据包的基本组成 1. 波特率 本章中主要讲解的是串口异步通讯,异步通讯中由于没有时钟信号(如前面讲解的 DB9 接口中是没有时钟信号的),所以两个通讯设备之间需要约定好波特率,即每个码元的长度, 以便对信号进行解码,图 20-6 中用虚线分开的每一格就是代表一个码元。常见的波特率为 4800、9600、115200 等。 2. 通讯的起始和停止信号 串口通讯的一个数据包从起始信号开始,直到停止信号结束。数据包的起始信号由一 个逻辑 0 的数据位表示,而数据包的停止信号可由 0.5、1、1.5 或 2 个逻辑 1 的数据位表示, 只要双方约定一致即可。 3. 有效数据 在数据包的起始位之后紧接着的就是要传输的主体数据内容,也称为有效数据,有效 数据的长度常被约定为 5、6、7 或 8 位长。 4. 数据校验 在有效数据之后,有一个可选的数据校验位。由于数据通信相对更容易受到外部干扰 导致传输数据出现偏差,可以在传输过程加上校验位来解决这个问题。校验方法有奇校验 (odd)、偶校验(even)、0 校验(space)、1 校验(mark)以及无校验(noparity),它们介绍如下:  奇校验要求有效数据和校验位中“1”的个数为奇数,比如一个 8 位长的有效数据 为:01101001,此时总共有 4 个“1”,为达到奇校验效果,校验位为“1”,最 后传输的数据将是 8 位的有效数据加上 1 位的校验位总共 9 位。 第 167 页 共 928 零死角玩转 STM32—F429  偶校验与奇校验要求刚好相反,要求帧数据和校验位中“1”的个数为偶数,比如 数据帧:11001010,此时数据帧“1”的个数为 4 个,所以偶校验位为“0”。  0 校验是不管有效数据中的内容是什么,校验位总为“0”,1 校验是校验位总为 “1”。  在无校验的情况下,数据包中不包含校验位。 20.2 STM32 的 USART 简介 STM32 芯片具有多个 USART 外设用于串口通讯,它是 Universal Synchronous Asynchronous Receiver and Transmitter 的缩写,即通用同步异步收发器可以灵活地与外部设 备进行全双工数据交换。有别于 USART,它还有具有 UART 外设(Universal Asynchronous Receiver and Transmitter),它是在 USART 基础上裁剪掉了同步通信功能,只有异步通信。 简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用的串口通信基 本都是 UART。 USART 满足外部设备对工业标准 NRZ 异步串行数据格式的要求,并且使用了小数波 特率发生器,可以提供多种波特率,使得它的应用更加广泛。USART 支持同步单向通信和 半双工单线通信;还支持局域互连网络 LIN、智能卡(SmartCard)协议与 lrDA(红外线数据 协会) SIR ENDEC 规范。 USART 支持使用 DMA,可实现高速数据通信,有关 DMA 具体应用将在 DMA 章节 作具体讲解。 USART 在 STM32 应用最多莫过于“打印”程序信息,一般在硬件设计时都会预留一 个 USART 通信接口连接电脑,用于在调试程序是可以把一些调试信息“打印”在电脑端 的串口调试助手工具上,从而了解程序运行是否正确、指出运行出错位置等等。 STM32 的 USART 输出的是 TTL 电平信号,若需要 RS-232 标准的信号可使用 MAX3232 芯片进行转换。 20.3 USART 功能框图 STM32 的 USART 功能框图包含了 USART 最核心内容,掌握了功能框图,对 USART 就有一个整体的把握,在编程时就思路就非常清晰,见图 20-7。 第 168 页 共 928 零死角玩转 STM32—F429 图 20-7 USART 功能框图 1. ①功能引脚 TX:发送数据输出引脚。 RX:接收数据输入引脚。 SW_RX:数据接收引脚,只用于单线和智能卡模式,属于内部引脚,没有具体外部引 脚。 nRTS:请求以发送(Request To Send),n 表示低电平有效。如果使能 RTS 流控制,当 USART 接收器准备好接收新数据时就会将 nRTS 变成低电平;当接收寄存器已满时, nRTS 将被设置为高电平。该引脚只适用于硬件流控制。 第 169 页 共 928 零死角玩转 STM32—F429 nCTS:清除以发送(Clear To Send),n 表示低电平有效。如果使能 CTS 流控制,发送 器在发送下一帧数据之前会检测 nCTS 引脚,如果为低电平,表示可以发送数据,如果为 高电平则在发送完当前数据帧之后停止发送。该引脚只适用于硬件流控制。 SCLK:发送器时钟输出引脚。这个引脚仅适用于同步模式。 USART 引脚在 STM32F429IGT6 芯片具体发布见表 20-3。 表 20-3 STM32F429IGT6 芯片的 USART 引脚 APB2(最高 90MHz) APB1(最高 45MHz) USART1 USART6 USART2 USART3 UART4 UART5 UART7 UART8 TX PB10/PD8 PA9/PB6 PC6/PG14 PA2/PD5 PA0/PC10 PC12 PF7/PE8 PE1 /PC10 RX PA10/PB7 PC7/PG9 PB11/PD9 PA3/PD6 /PC11 PA1/PC11 PD2 PF6/PE7 PE0 SCLK PA8 PG7/PC8 PA4/PD7 PB12/PD10 /PC12 nCTS PA11 PG13/PG15 PA0/PD3 PB13/PD11 nRTS PA12 PG8/PG12 PA1/PD4 PB14/PD12 STM32F42xxx 系统控制器有四个 USART 和四个 UART,其中 USART1 和 USART6 的 时钟来源于 APB2 总线时钟,其最大频率为 90MHz,其他六个的时钟来源于 APB1 总线时 钟,其最大频率为 45MHz。 UART 只是异步传输功能,所以没有 SCLK、nCTS 和 nRTS 功能引脚。 观察表 20-3 可发现很多 USART 的功能引脚有多个引脚可选,这非常方便硬件设计, 只要在程序编程时软件绑定引脚即可。 2. ②数据寄存器 USART 数据寄存器(USART_DR)只有低 9 位有效,并且第 9 位数据是否有效要取决于 USART 控制寄存器 1(USART_CR1)的 M 位设置,当 M 位为 0 时表示 8 位数据字长,当 M 位为 1 表示 9 位数据字长,我们一般使用 8 位数据字长。 USART_DR 包含了已发送的数据或者接收到的数据。USART_DR 实际是包含了两个 寄存器,一个专门用于发送的可写 TDR,一个专门用于接收的可读 RDR。当进行发送操 作时,往 USART_DR 写入数据会自动存储在 TDR 内;当进行读取操作时,向 USART_DR 读取数据会自动提取 RDR 数据。 TDR 和 RDR 都是介于系统总线和移位寄存器之间。串行通信是一个位一个位传输的, 发送时把 TDR 内容转移到发送移位寄存器,然后把移位寄存器数据每一位发送出去,接收 时把接收到的每一位顺序保存在接收移位寄存器内然后才转移到 RDR。 USART 支持 DMA 传输,可以实现高速数据传输,具体 DMA 使用将在 DMA 章节讲 解。 第 170 页 共 928 零死角玩转 STM32—F429 3. ③控制器 USART 有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等等。 使用 USART 之前需要向 USART_CR1 寄存器的 UE 位置 1 使能 USART。发送或者接收数 据字长可选 8 位或 9 位,由 USART_CR1 的 M 位控制。 发送器 当 USART_CR1 寄存器的发送使能位 TE 置 1 时,启动数据发送,发送移位寄存器的 数据会在 TX 引脚输出,如果是同步模式 SCLK 也输出时钟信号。 一个字符帧发送需要三个部分:起始位+数据帧+停止位。起始位是一个位周期的低电 平,位周期就是每一位占用的时间;数据帧就是我们要发送的 8 位或 9 位数据,数据是从 最低位开始传输的;停止位是一定时间周期的高电平。 停止位时间长短是可以通过 USART 控制寄存器 2(USART_CR2)的 STOP[1:0]位控制, 可选 0.5 个、1 个、1.5 个和 2 个停止位。默认使用 1 个停止位。2 个停止位适用于正常 USART 模式、单线模式和调制解调器模式。0.5 个和 1.5 个停止位用于智能卡模式。 当选择 8 位字长,使用 1 个停止位时,具体发送字符时序图见图 20-8。 图 20-8 字符发送时序图 当发送使能位 TE 置 1 之后,发送器开始会先发送一个空闲帧(一个数据帧长度的高电 平),接下来就可以往 USART_DR 寄存器写入要发送的数据。在写入最后一个数据后,需 要等待 USART 状态寄存器(USART_SR)的 TC 位为 1,表示数据传输完成,如果 USART_CR1 寄存器的 TCIE 位置 1,将产生中断。 在发送数据时,编程的时候有几个比较重要的标志位我们来总结下。 名称 描述 TE TXE TC TXIE 发送使能 发送寄存器为空,发送单个字节的时候使用 发送完成,发送多个字节数据的时候使用 发送完成中断使能 接收器 如果将 USART_CR1 寄存器的 RE 位置 1,使能 USART 接收,使得接收器在 RX 线开 始搜索起始位。在确定到起始位后就根据 RX 线电平状态把数据存放在接收移位寄存器内。 第 171 页 共 928 零死角玩转 STM32—F429 接收完成后就把接收移位寄存器数据移到 RDR 内,并把 USART_SR 寄存器的 RXNE 位置 1,同时如果 USART_CR2 寄存器的 RXNEIE 置 1 的话可以产生中断。 在接收数据时,编程的时候有几个比较重要的标志位我们来总结下。 名称 描述 RE RXNE RXNEIE 接收使能 读数据寄存器非空 发送完成中断使能 为得到一个信号真实情况,需要用一个比这个信号频率高的采样信号去检测,称为过 采样,这个采样信号的频率大小决定最后得到源信号准确度,一般频率越高得到的准确度 越高,但为了得到越高频率采样信号越也困难,运算和功耗等等也会增加,所以一般选择 合适就好。 接收器可配置为不同过采样技术,以实现从噪声中提取有效的数据。USART_CR1 寄 存器的 OVER8 位用来选择不同的采样采样方法,如果 OVER8 位设置为 1 采用 8 倍过采样, 即用 8 个采样信号采样一位数据;如果 OVER8 位设置为 0 采用 16 倍过采样,即用 16 个采 样信号采样一位数据。 USART 的起始位检测需要用到特定序列。如果在 RX 线识别到该特定序列就认为是检 测到了起始位。起始位检测对使用 16 倍或 8 倍过采样的序列都是一样的。该特定序列为: 1110X0X0X0000,其中 X 表示电平任意,1 或 0 皆可。 8 倍过采样速度更快,最高速度可达 fPCLK/8,fPCLK 为 USART 时钟,采样过程见图 20-9。使用第 4、5、6 次脉冲的值决定该位的电平状态。 图 20-9 8 倍过采样过程 16 倍过采样速度虽然没有 8 倍过采样那么快,但得到的数据更加精准,其最大速度为 fPCLK/16,采样过程见图 20-10。使用第 8、9、10 次脉冲的值决定该位的电平状态。 第 172 页 共 928 零死角玩转 STM32—F429 图 20-10 16 倍过采样过程 4. ④小数波特率生成 波特率指数据信号对载波的调制速率,它用单位时间内载波调制状态改变次数来表示, 单位为波特。比特率指单位时间内传输的比特数,单位 bit/s(bps)。对于 USART 波特率与 比特率相等,以后不区分这两个概念。波特率越大,传输速率越快。 USART 的发送器和接收器使用相同的波特率。计算公式如下: 公式 20-1 波特率计算 其中,fPLCK 为 USART 时钟,参考表 20-3;OVER8 为 USART_CR1 寄存器的 OVER8 位对应的值,USARTDIV 是一个存放在波特率寄存器(USART_BRR)的一个无符号定点数。 其中 DIV_Mantissa[11:0]位定义 USARTDIV 的整数部分,DIV_Fraction[3:0]位定义 USARTDIV 的小数部分,DIV_Fraction[3]位只有在 OVER8 位为 0 时有效,否则必须清零。 例如,如果 OVER8=0,DIV_Mantissa=24 且 DIV_Fraction=10,此时 USART_BRR 值 为 0x18A;那么 USARTDIV 的小数位 10/16=0.625;整数位 24,最终 USARTDIV 的值为 24.625。 如果 OVER8=0 并且知道 USARTDIV 值为 27.68,那么 DIV_Fraction=16*0.68=10.88, 最接近的正整数为 11,所以 DIV_Fraction[3:0]为 0xB;DIV_Mantissa=整数(27.68)=27,即 位 0x1B。 如果 OVER8=1 情况类似,只是把计算用到的权值由 16 改为 8。 波特率的常用值有 2400、9600、19200、115200。下面以实例讲解如何设定寄存器值 得到波特率的值。 第 173 页 共 928 零死角玩转 STM32—F429 由表 20-3 可知 USART1 和 USART6 使用 APB2 总线时钟,最高可达 90MHz,其他 USART 的最高频率为 45MHz。我们选取 USART1 作为实例讲解,即 fPLCK=90MHz。 当我们使用 16 倍过采样时即 OVER8=0,为得到 115200bps 的波特率,此时: 解得 USARTDIV=48.825125,可算得 DIV_Fraction=0xD,DIV_Mantissa=0x30,即应 该设置 USART_BRR 的值为 0x30D。 在计算 DIV_Fraction 时经常出现小数情况,经过我们取舍得到整数,这样会导致最终 输出的波特率较目标值略有偏差。下面我们从 USART_BRR 的值为 0x30D 开始计算得出实 际输出的波特率大小。 由 USART_BRR 的值为 0x30D,可得 DIV_Fraction=13,DIV_Mantissa=48,所以 USARTDIV=48+16*0.13=48.8125,所以实际波特率为:115237;这个值跟我们的目标波特 率误差为 0.03%,这么小的误差在正常通信的允许范围内。 8 倍过采样时计算情况原理是一样的。 5. 校验控制 STM32F4xx 系列控制器 USART 支持奇偶校验。当使用校验位时,串口传输的长度将 是 8 位的数据帧加上 1 位的校验位总共 9 位,此时 USART_CR1 寄存器的 M 位需要设置为 1,即 9 数据位。将 USART_CR1 寄存器的 PCE 位置 1 就可以启动奇偶校验控制,奇偶校 验由硬件自动完成。启动了奇偶校验控制之后,在发送数据帧时会自动添加校验位,接收 数据时自动验证校验位。接收数据时如果出现奇偶校验位验证失败,会见 USART_SR 寄存 器的 PE 位置 1,并可以产生奇偶校验中断。 使能了奇偶校验控制后,每个字符帧的格式将变成:起始位+数据帧+校验位+停止位。 6. 中断控制 USART 有多个中断请求事件,具体见表 20-4。 表 20-4 USART 中断请求 中断事件 发送数据寄存器为空 CTS 标志 发送完成 准备好读取接收到的数据 检测到上溢错误 检测到空闲线路 事件标志 使能控制位 TXE TXEIE CTS CTSIE TC TCIE RXNE ORE RXNEIE IDLE IDLEIE 第 174 页 共 928 零死角玩转 STM32—F429 奇偶校验错误 PE PEIE 断路标志 LBD LBDIE 多缓冲通信中的噪声标志、 上溢错误和帧错误 NF/ORE/FE EIE 20.4 USART 初始化结构体详解 标准库函数对每个外设都建立了一个初始化结构体,比如 USART_InitTypeDef,结构 体成员用于设置外设工作参数,并由外设初始化配置函数,比如 USART_Init()调用,这些 设定参数将会设置外设相应的寄存器,达到配置外设工作环境的目的。 初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个 成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 stm32f4xx_usart.h 文件 中,初始化库函数定义在 stm32f4xx_usart.c 文件中,编程时我们可以结合这两个文件内注 释使用。 USART 初始化结构体 1 typedef struct { 2 uint32_t USART_BaudRate; // 波特率 3 uint16_t USART_WordLength; // 字长 4 uint16_t USART_StopBits; // 停止位 5 uint16_t USART_Parity; // 校验位 6 uint16_t USART_Mode; // USART 模式 7 uint16_t USART_HardwareFlowControl; // 硬件流控制 8 } USART_InitTypeDef; 1) USART_BaudRate:波特率设置。一般设置为 2400、9600、19200、115200。标准 库 函 数 会 根 据 设 定 值 计 算 得 到 USARTDIV 值 , 见 公 式 20-1 , 并 设 置 USART_BRR 寄存器值。 2) USART_WordLength:数据帧字长,可选 8 位或 9 位。它设定 USART_CR1 寄存 器的 M 位的值。如果没有使能奇偶校验控制,一般使用 8 数据位;如果使能了奇 偶校验则一般设置为 9 数据位。 3) USART_StopBits:停止位设置,可选 0.5 个、1 个、1.5 个和 2 个停止位,它设定 USART_CR2 寄存器的 STOP[1:0]位的值,一般我们选择 1 个停止位。 4) USART_Parity : 奇 偶 校 验 控 制 选 择 , 可 选 USART_Parity_No( 无 校 验 ) 、 USART_Parity_Even( 偶 校 验 ) 以 及 USART_Parity_Odd( 奇 校 验 ) , 它 设 定 USART_CR1 寄存器的 PCE 位和 PS 位的值。 5) USART_Mode:USART 模式选择,有 USART_Mode_Rx 和 USART_Mode_Tx, 允许使用逻辑或运算选择两个,它设定 USART_CR1 寄存器的 RE 位和 TE 位。 6) USART_HardwareFlowControl:硬件流控制选择,只有在硬件流控制模式才有效, 可选有⑴使能 RTS、⑵使能 CTS、⑶同时使能 RTS 和 CTS、⑷不使能硬件流。 第 175 页 共 928 零死角玩转 STM32—F429 当使用同步模式时需要配置 SCLK 引脚输出脉冲的属性,标准库使用一个时钟初始化 结构体 USART_ClockInitTypeDef 来设置,因此该结构体内容也只有在同步模式才需要设 置。 USART 时钟初始化结构体 1 typedef struct { 2 uint16_t USART_Clock; 3 uint16_t USART_CPOL; 4 uint16_t USART_CPHA; 5 uint16_t USART_LastBit; 6 } USART_ClockInitTypeDef; // 时钟使能控制 // 时钟极性 // 时钟相位 // 最尾位时钟脉冲 1) USART_Clock:同步模式下 SCLK 引脚上时钟输出使能控制,可选禁止时钟输出 (USART_Clock_Disable)或开启时钟输出(USART_Clock_Enable);如果使用同步模 式发送,一般都需要开启时钟。它设定 USART_CR2 寄存器的 CLKEN 位的值。 2) USART_CPOL:同步模式下 SCLK 引脚上输出时钟极性设置,可设置在空闲时 SCLK 引脚为低电平(USART_CPOL_Low)或高电平(USART_CPOL_High)。它设 定 USART_CR2 寄存器的 CPOL 位的值。 3) USART_CPHA:同步模式下 SCLK 引脚上输出时钟相位设置,可设置在时钟第一 个变化沿捕获数据(USART_CPHA_1Edge)或在时钟第二个变化沿捕获数据。它设 定 USART_CR2 寄存器的 CPHA 位的值。USART_CPHA 与 USART_CPOL 配合 使用可以获得多种模式时钟关系。 4) USART_LastBit:选择在发送最后一个数据位的时候时钟脉冲是否在 SCLK 引脚 输 出 , 可 以 是 不 输 出 脉 冲 (USART_LastBit_Disable) 、 输 出 脉 冲 (USART_LastBit_Enable)。它设定 USART_CR2 寄存器的 LBCL 位的值。 20.5 USART1 接发通信实验 USART 只需两根信号线即可完成双向通信,对硬件要求低,使得很多模块都预留 USART 接口来实现与其他模块或者控制器进行数据传输,比如 GSM 模块,WIFI 模块、蓝 牙模块等等。在硬件设计时,注意还需要一根“共地线”。 我们经常使用 USART 来实现控制器与电脑之间的数据传输。这使得我们调试程序非 常方便,比如我们可以把一些变量的值、函数的返回值、寄存器标志位等等通过 USART 发送到串口调试助手,这样我们可以非常清楚程序的运行状态,当我们正式发布程序时再 把这些调试信息去除即可。 我们不仅仅可以将数据发送到串口调试助手,我们还可以在串口调试助手发送数据给 控制器,控制器程序根据接收到的数据进行下一步工作。 首先,我们来编写一个程序实现开发板与电脑通信,在开发板上电时通过 USART 发 送一串字符串给电脑,然后开发板进入中断接收等待状态,如果电脑有发送数据过来,开 发板就会产生中断,我们在中断服务函数接收数据,并马上把数据返回发送给电脑。 第 176 页 共 928 零死角玩转 STM32—F429 20.5.1 硬件设计 为利用 USART 实现开发板与电脑通信,需要用到一个 USB 转 USART 的 IC,我们选 择 CH340G 芯片来实现这个功能,CH340G 是一个 USB 总线的转接芯片,实现 USB 转 USART、USB 转 IrDA 红外或者 USB 转打印机接口,我们使用其 USB 转 USART 功能。具 体电路设计见图 20-11。 我们将 CH340G 的 TXD 引脚与 USART1 的 RX 引脚连接,CH340G 的 RXD 引脚与 USART1 的 TX 引脚连接。CH340G 芯片集成在开发板上,其地线(GND)已与控制器的 GND 连通。 图 20-11 USB 转串口硬件设计 20.5.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_debug_usart.c 和 bsp_debug_usart.h 文件用来存放 USART 驱动程序及相关宏定义。 1. 编程要点 1) 使能 RX 和 TX 引脚 GPIO 时钟和 USART 时钟; 2) 初始化 GPIO,并将 GPIO 复用到 USART 上; 3) 配置 USART 参数; 4) 配置中断控制器并使能 USART 接收中断; 第 177 页 共 928 零死角玩转 STM32—F429 5) 使能 USART; 6) 在 USART 接收中断服务函数实现数据接收和发送。 2. 代码分析 GPIO 和 USART 宏定义 代码清单 20-1 GPIO 和 USART 宏定义 1 #define DEBUG_USART 2 #define DEBUG_USART_CLK 3 #define DEBUG_USART_BAUDRATE 4 5 #define DEBUG_USART_RX_GPIO_PORT 6 #define DEBUG_USART_RX_GPIO_CLK 7 #define DEBUG_USART_RX_PIN 8 #define DEBUG_USART_RX_AF 9 #define DEBUG_USART_RX_SOURCE 10 11 #define DEBUG_USART_TX_GPIO_PORT 12 #define DEBUG_USART_TX_GPIO_CLK 13 #define DEBUG_USART_TX_PIN 14 #define DEBUG_USART_TX_AF 15 #define DEBUG_USART_TX_SOURCE 16 17 #define DEBUG_USART_IRQHandler 18 #define DEBUG_USART_IRQ USART1 RCC_APB2Periph_USART1 115200 //串口波特率 GPIOA RCC_AHB1Periph_GPIOA GPIO_Pin_10 GPIO_AF_USART1 GPIO_PinSource10 GPIOA RCC_AHB1Periph_GPIOA GPIO_Pin_9 GPIO_AF_USART1 GPIO_PinSource9 USART1_IRQHandler USART1_IRQn 使用宏定义方便程序移植和升级,根据图 20-11 电路,我们选择使用 USART1,设定 波特率为 115200,一般我们会默认使用“8-N-1”参数,即 8 个数据位、不用校验、一位 停止位。查阅表 20-3 可知 USART1 的 TX 线可对于 PA9 和 PB6 引脚,RX 线可对于 PA10 和 PB7 引脚,这里我们选择 PA9 以及 PA10 引脚。最后定义中断相关参数。 嵌套向量中断控制器 NVIC 配置 代码清单 20-2 中断控制器 NVIC 配置 1 static void NVIC_Configuration(void) 2{ 3 NVIC_InitTypeDef NVIC_InitStructure; 4 5 /* 嵌套向量中断控制器组选择 */ 6 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 7 8 /* 配置 USART 为中断源 */ 9 NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ; 10 /* 抢断优先级为 1 */ 11 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 12 /* 子优先级为 1 */ 13 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 14 /* 使能中断 */ 15 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 16 /* 初始化配置 NVIC */ 17 NVIC_Init(&NVIC_InitStructure); 18 } 19 第 178 页 共 928 零死角玩转 STM32—F429 在中断章节已对嵌套向量中断控制器的工作机制做了详细的讲解,这里我们就直接使 用它,配置 USART 作为中断源,因为本实验没有使用其他中断,对优先级什么具体要求。 USART 初始化配置 代码清单 20-3 USART 初始化配置 1 void Debug_USART_Config(void) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 USART_InitTypeDef USART_InitStructure; 5 /* 使能 USART GPIO 时钟 */ 6 RCC_AHB1PeriphClockCmd(DEBUG_USART_RX_GPIO_CLK | 7 DEBUG_USART_TX_GPIO_CLK, 8 ENABLE); 9 10 /* 使能 USART 时钟 */ 11 RCC_APB2PeriphClockCmd(DEBUG_USART_CLK, ENABLE); 12 13 /* GPIO 初始化 */ 14 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 15 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; 16 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 17 18 /* 配置 Tx 引脚为复用功能 */ 19 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 20 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_PIN ; 21 GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure); 22 23 /* 配置 Rx 引脚为复用功能 */ 24 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 25 GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_PIN; 26 GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure); 27 28 /* 连接 PXx 到 USARTx_Tx*/ 29 GPIO_PinAFConfig(DEBUG_USART_RX_GPIO_PORT, 30 DEBUG_USART_RX_SOURCE, 31 DEBUG_USART_RX_AF); 32 33 /* 连接 PXx 到 USARTx__Rx*/ 34 GPIO_PinAFConfig(DEBUG_USART_TX_GPIO_PORT, 35 DEBUG_USART_TX_SOURCE, 36 DEBUG_USART_TX_AF); 37 38 /* 配置串 DEBUG_USART 模式 */ 39 /* 波特率设置:DEBUG_USART_BAUDRATE */ 40 USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE; 41 /* 字长(数据位+校验位):8 */ 42 USART_InitStructure.USART_WordLength = USART_WordLength_8b; 43 /* 停止位:1 个停止位 */ 44 USART_InitStructure.USART_StopBits = USART_StopBits_1; 45 /* 校验位选择:不使用校验 */ 46 USART_InitStructure.USART_Parity = USART_Parity_No; 47 /* 硬件流控制:不使用硬件流 */ 48 USART_InitStructure.USART_HardwareFlowControl = 49 USART_HardwareFlowControl_None; 50 /* USART 模式控制:同时使能接收和发送 */ 51 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; 52 /* 完成 USART 初始化配置 */ 53 USART_Init(DEBUG_USART, &USART_InitStructure); 54 55 /* 嵌套向量中断控制器 NVIC 配置 */ 56 NVIC_Configuration(); 第 179 页 共 928 零死角玩转 STM32—F429 57 58 59 60 61 62 63 } 64 /* 使能串口接收中断 */ USART_ITConfig(DEBUG_USART, USART_IT_RXNE, ENABLE); /* 使能串口 */ USART_Cmd(DEBUG_USART, ENABLE); 使用 GPIO_InitTypeDef 和 USART_InitTypeDef 结构体定义一个 GPIO 初始化变量以及 一个 USART 初始化变量,这两个结构体内容我们之前已经有详细讲解。 调用 RCC_AHB1PeriphClockCmd 函数开启 GPIO 端口时钟,使用 GPIO 之前必须开启 对应端口的时钟。使用 RCC_APB2PeriphClockCmd 函数开启 USART 时钟。 使用 GPIO 之前都需要初始化配置它,并且还要添加特殊设置,因为我们使用它作为 外设的引脚,一般都有特殊功能。我们在初始化时需要把它的模式设置为复用功能。 每个 GPIO 都可以作为多个外设的特殊功能引脚,比如 PA10 这个引脚不仅仅可以作为 普通的输入\输出引脚,还可以作为 USART1 的 RX 线引脚(USART1_RX)、定时器 1 通道 3 引脚(TIM1_CH3)、全速 OTG 的 ID 引脚(OTG_FS_ID)以及 DCMI 的数据 1 引脚(DCMI_D1) 这四个外设的功能引脚,我们只能从中选择一个使用,这时就通过 GPIO 引脚复用功能配 置(GPIO_PinAFConfig)函数实现复用功能引脚的连接。 这时我们可能会想如果程序把 PA10 用于 TIM1_CH3,此时 USART1_RX 就没办法使 用了,那岂不是不能使用 USART1 了,实际上情况没有这么糟糕的,查阅表 20-3 我们可以 看到 USART1_RX 不仅仅只有 PA10,还可以是 PB7。所以此时我们可以 PB7 这个引脚来 实现 USART1 通信。那要是 PB7 也是被其他外设占用了呢?那就没办法了,只能使用其他 USART。 GPIO_PinAFConfig 函数接收三个参数,第一个参数为 GPIO 端口,比如 GPIOA;第二 个参数是指定要复用的引脚号,比如 GPIO_PinSource10;第三个参数是选择复用外设,比 如 GPIO_AF_USART1。该函数最终操作的是 GPIO 复用功能寄存器 GPIO_AFRH 和 GPIO_AFRL,分高低两个。 接下来,我们配置 USART1 通信参数并调用 USART 初始化函数完成配置。 程序用到 USART 接收中断,需要配置 NVIC,这里调用 NVIC_Configuration 函数完成 配置。配置 NVIC 就可以调用 USART_ITConfig 函数使能 USART 接收中断。 最后调用 USART_Cmd 函数使能 USART。 字符发送 代码清单 20-4 字符发送函数 1 /***************** 发送一个字符 **********************/ 2 void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch) 3{ 4 /* 发送一个字节数据到 USART */ 5 USART_SendData(pUSARTx,ch); 6 7 /* 等待发送数据寄存器为空 */ 8 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); 9} 10 第 180 页 共 928 零死角玩转 STM32—F429 11 /***************** 发送字符串 **********************/ 12 void Usart_SendString( USART_TypeDef * pUSARTx, char *str) 13 { 14 unsigned int k=0; 15 do { 16 Usart_SendByte( pUSARTx, *(str + k) ); 17 k++; 18 } while (*(str + k)!='\0'); 19 20 /* 等待发送完成 */ 21 while (USART_GetFlagStatus(pUSARTx,USART_FLAG_TC)==RESET) { 22 } 23 } Usart_SendByte 函数用来在指定 USART 发送一个 ASCLL 码值字符,它有两个形参, 第一个为 USART,第二个为待发送的字符。它是通过调用库函数 USART_SendData 来实 现的,并且增加了等待发送完成功能。通过使用 USART_GetFlagStatus 函数来获取 USART 事件标志来实现发送完成功能等待,它接收两个参数,一个是 USART,一个是事件标志。 这里我们循环检测发送数据寄存器为空这个标志,当跳出 while 循环时说明发送数据寄存 器为空这个事实。 Usart_SendString 函数用来发送一个字符串,它实际是调用 Usart_SendByte 函数发送每 个字符,直到遇到空字符才停止发送。最后使用循环检测发送完成的事件标志来实现保证 数据发送完成后才退出函数。 USART 中断服务函数 代码清单 20-5 USART 中断服务函数 1 void DEBUG_USART_IRQHandler(void) 2{ 3 uint8_t ucTemp; 4 if (USART_GetITStatus(DEBUG_USART,USART_IT_RXNE)!=RESET) { 5 ucTemp = USART_ReceiveData( DEBUG_USART ); 6 USART_SendData(DEBUG_USART,ucTemp); 7 } 8 9} 这段代码是存放在 stm32f4xx_it.c 文件中的,该文件用来集中存放外设中断服务函数。 当我们使能了中断并且中断发生时就会执行中断服务函数。 我们在代码清单 20-3 使能了 USART 接收中断,当 USART 有接收到数据就会执行 DEBUG_USART_IRQHandler 函数。USART_GetITStatus 函数与 USART_GetFlagStatus 函 数类似用来获取标志位状态,但 USART_GetITStatus 函数是专门用来获取中断事件标志的, 并返回该标志位状态。使用 if 语句来判断是否是真的产生 USART 数据接收这个中断事件, 如果是真的就使用 USART 数据读取函数 USART_ReceiveData 读取数据到指定存储区。然 后再调用 USART 数据发送函数 USART_SendData 把数据又发送给源设备。 主函数 代码清单 20-6 主函数 1 int main(void) 2{ 第 181 页 共 928 零死角玩转 STM32—F429 3 4 5 6 7 8 9 10 11 } /*初始化 USART 配置模式为 115200 8-N-1,中断接收*/ Debug_USART_Config(); Usart_SendString( DEBUG_USART,"这是一个串口中断接收回显实验\n"); while (1) { } 首先我们需要调用 Debug_USART_Config 函数完成 USART 初始化配置,包括 GPIO 配置,USART 配置,接收中断使用等等信息。 接下来就可以调用字符发送函数把数据发送给串口调试助手了。 最后主函数什么都不做,只是静静地等待 USART 接收中断的产生,并在中断服务函 数把数据回传。 20.5.3 下载验证 保证开发板相关硬件连接正确,用 USB 线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板,此时串口调试助手即可收到 开发板发过来的数据。我们在串口调试助手发送区域输入任意字符,点击发送按钮,马上 在串口调试助手接收区即可看到相同的字符。 图 20-12 实验现象 第 182 页 共 928 零死角玩转 STM32—F429 20.6 USART1 指令控制 RGB 彩灯实验 在学习 C 语言时我们经常使用 C 语言标准函数库输入输出函数,比如 printf、scanf、 getchar 等等。为让开发板也支持这些函数需要把 USART 发送和接收函数添加到这些函数 的内部函数内。 正如之前所讲,可以在串口调试助手输入指令,让开发板根据这些指令执行一些任务, 现在我们编写让程序接收 USART 数据,根据数据内容控制 RGB 彩灯的颜色。 20.6.1 硬件设计 硬件设计同第一个实验。 20.6.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp _usart.c 和 bsp _usart.h 文件用来 存放 USART 驱动程序及相关宏定义。 1. 编程要点 1) 初始化配置 RGB 彩色灯 GPIO; 2) 使能 RX 和 TX 引脚 GPIO 时钟和 USART 时钟; 3) 初始化 GPIO,并将 GPIO 复用到 USART 上; 4) 配置 USART 参数; 5) 使能 USART; 6) 获取指令输入,根据指令控制 RGB 彩色灯。 2. 代码分析 GPIO 和 USART 宏定义 代码清单 20-7 GPIO 和 USART 宏定义 1 //引脚定义 2 /*******************************************************/ 3 #define USARTx USART1 4 5 /* 不同的串口挂载的总线不一样,时钟使能函数也不一样,移植时要注意 6 * 串口 1 和 6 是 RCC_APB2PeriphClockCmd 7 * 串口 2/3/4/5/7 是 RCC_APB1PeriphClockCmd 8 */ 9 #define USARTx_CLK RCC_APB2Periph_USART1 10 #define USARTx_CLOCKCMD RCC_APB2PeriphClockCmd 11 #define USARTx_BAUDRATE 115200 //串口波特率 第 183 页 共 928 零死角玩转 STM32—F429 12 13 #define USARTx_RX_GPIO_PORT GPIOA 14 #define USARTx_RX_GPIO_CLK RCC_AHB1Periph_GPIOA 15 #define USARTx_RX_PIN GPIO_Pin_10 16 #define USARTx_RX_AF GPIO_AF_USART1 17 #define USARTx_RX_SOURCE GPIO_PinSource10 18 19 #define USARTx_TX_GPIO_PORT GPIOA 20 #define USARTx_TX_GPIO_CLK RCC_AHB1Periph_GPIOA 21 #define USARTx_TX_PIN GPIO_Pin_9 22 #define USARTx_TX_AF GPIO_AF_USART1 23 #define USARTx_TX_SOURCE GPIO_PinSource9 24 25 /************************************************************/ 使用宏定义方便程序移植和升级,这里我们可以 USART1,设定波特率为 115200。 USART 初始化配置 代码清单 20-8 USART 初始化配置 1 void USARTx_Config(void) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 USART_InitTypeDef USART_InitStructure; 5 6 RCC_AHB1PeriphClockCmd(USARTx_RX_GPIO_CLK|USARTx_TX_GPIO_CLK,ENABLE); 7 8 /* 使能 USART 时钟 */ 9 USARTx_CLOCKCMD(USARTx_CLK, ENABLE); 10 11 /* GPIO 初始化 */ 12 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 13 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; 14 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 15 16 /* 配置 Tx 引脚为复用功能 */ 17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 18 GPIO_InitStructure.GPIO_Pin = USARTx_TX_PIN ; 19 GPIO_Init(USARTx_TX_GPIO_PORT, &GPIO_InitStructure); 20 21 /* 配置 Rx 引脚为复用功能 */ 22 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 23 GPIO_InitStructure.GPIO_Pin = USARTx_RX_PIN; 24 GPIO_Init(USARTx_RX_GPIO_PORT, &GPIO_InitStructure); 25 26 /* 连接 PXx 到 USARTx_Tx*/ 27 GPIO_PinAFConfig(USARTx_RX_GPIO_PORT,USARTx_RX_SOURCE,USARTx_RX_AF); 28 29 /* 连接 PXx 到 USARTx__Rx*/ 30 GPIO_PinAFConfig(USARTx_TX_GPIO_PORT,USARTx_TX_SOURCE,USARTx_TX_AF); 31 32 /* 配置串 DEBUG_USART 模式 */ 33 /* 波特率设置:DEBUG_USART_BAUDRATE */ 34 USART_InitStructure.USART_BaudRate = USARTx_BAUDRATE; 35 /* 字长(数据位+校验位):8 */ 36 USART_InitStructure.USART_WordLength = USART_WordLength_8b; 37 /* 停止位:1 个停止位 */ 38 USART_InitStructure.USART_StopBits = USART_StopBits_1; 39 /* 校验位选择:偶校验 */ 40 USART_InitStructure.USART_Parity = USART_Parity_No; 41 /* 硬件流控制:不使用硬件流 */ 42 USART_InitStructure.USART_HardwareFlowControl = 43 USART_HardwareFlowControl_None; 44 /* USART 模式控制:同时使能接收和发送 */ 第 184 页 共 928 零死角玩转 STM32—F429 45 46 47 48 49 50 51 } USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; /* 完成 USART 初始化配置 */ USART_Init(USARTx, &USART_InitStructure); /* 使能串口 */ USART_Cmd(USARTx, ENABLE); 使用 GPIO_InitTypeDef 和 USART_InitTypeDef 结构体定义一个 GPIO 初始化变量以及 一个 USART 初始化变量,这两个结构体内容我们之前已经有详细讲解。 调用 RCC_AHB1PeriphClockCmd 函数开启 GPIO 端口时钟,使用 GPIO 之前必须开启 对应端口的时钟。 初始化配置 RX 线和 TX 线引脚为复用功能,并将指定的 GPIO 连接至 USART1,然后 配置串口的工作参数为 115200-8-N-1。最后调用 USART_Cmd 函数使能 USART。 重定向 prinft 和 scanf 函数 代码清单 20-9 重定向输入输出函数 1 ///重定向 c 库函数 printf 到串口,重定向后可使用 printf 函数 2 int fputc(int ch, FILE *f) 3{ 4 /* 发送一个字节数据到串口 */ 5 USART_SendData(USARTx, (uint8_t) ch); 6 7 /* 等待发送完毕 */ 8 while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); 9 10 return (ch); 11 } 12 13 ///重定向 c 库函数 scanf 到串口,重写向后可使用 scanf、getchar 等函数 14 int fgetc(FILE *f) 15 { 16 /* 等待串口输入数据 */ 17 while (USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) == RESET); 18 19 return (int)USART_ReceiveData(USARTx); 20 } 在 C 语言标准库中,fputc 函数是 printf 函数内部的一个函数,功能是将字符 ch 写入到 文件指针 f 所指向文件的当前写指针位置,简单理解就是把字符写入到特定文件中。我们 使用 USART 函数重新修改 fputc 函数内容,达到类似“写入”的功能。 fgetc 函数与 fputc 函数非常相似,实现字符读取功能。在使用 scanf 函数时需要注意字 符输入格式。 还有一点需要注意的,使用 fput 和 fgetc 函数达到重定向 C 语言标准库输入输出函数 必须在 MDK 的工程选项把“Use MicroLIB”勾选上,MicoroLIB 是缺省 C 库的备选库,它 对标准 C 库进行了高度优化使代码更少,占用更少资源。 为使用 printf、scanf 函数需要在文件中包含 stdio.h 头文件。 输出提示信息 代码清单 20-10 输出提示信息 1 static void Show_Message(void) 第 185 页 共 928 零死角玩转 STM32—F429 2{ 3 4 5 6 7 8 9 10 11 12 13 14 15 } printf("\r\n 这是一个通过串口通信指令控制 RGB 彩灯实验 \n"); printf("使用 USART1 参数为:%d 8-N-1 \n",USARTx_BAUDRATE); printf("开发板接到指令后控制 RGB 彩灯颜色,指令对应如下:\n"); printf(" 指令 ------ 彩灯颜色 \n"); printf(" 1 ------ 红 \n"); printf(" 2 ------ 绿 \n"); printf(" 3 ------ 蓝 \n"); printf(" 4 ------ 黄 \n"); printf(" 5 ------ 紫 \n"); printf(" 6 ------ 青 \n"); printf(" 7 ------ 白 \n"); printf(" 8 ------ 灭 \n"); Show_Message 函数全部是调用 printf 函数,“打印”实验操作信息到串口调试助手。 主函数 代码清单 20-11 主函数 1 int main(void) 2{ 3 char ch; 4 5 /* 初始化 RGB 彩灯 */ 6 LED_GPIO_Config(); 7 8 /* 初始化 USART 配置模式为 115200 8-N-1 */ 9 USARTx_Config(); 10 11 /* 打印指令输入提示信息 */ 12 Show_Message(); 13 while (1) 14 { 15 /* 获取字符指令 */ 16 ch=getchar(); 17 printf("接收到字符:%c\n",ch); 18 19 /* 根据字符指令控制 RGB 彩灯颜色 */ 20 switch (ch) 21 { 22 case '1': 23 LED_RED; 24 break; 25 case '2': 26 LED_GREEN; 27 break; 28 case '3': 29 LED_BLUE; 30 break; 31 case '4': 32 LED_YELLOW; 33 break; 34 case '5': 35 LED_PURPLE; 36 break; 37 case '6': 38 LED_CYAN; 39 break; 40 case '7': 41 LED_WHITE; 42 break; 43 case '8': 第 186 页 共 928 零死角玩转 STM32—F429 44 LED_RGBOFF; 45 break; 46 default: 47 /* 如果不是指定指令字符,打印提示信息 */ 48 Show_Message(); 49 break; 50 } 51 } 52 } 首先我们定义一个字符变量来存放接收到的字符。 接下来调用 LED_GPIO_Config 函数完成 RGB 彩色 GPIO 初始化配置,该函数定义在 bsp_led.c 文件内。 调用 USARTx_Config 函完成 USART 初始化配置。 Show_Message 函数使用 printf 函数打印实验指令说明信息。 getchar 函数用于等待获取一个字符,并返回字符。我们使用 ch 变量保持返回的字符, 接下来判断 ch 内容执行对应的程序了。 我们使用 switch 语句判断 ch 变量内容,并执行对应的功能程序。 20.6.3 下载验证 保证开发板相关硬件连接正确,用 USB 线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板,此时串口调试助手即可收到 开发板发过来的数据。我们在串口调试助手发送区域输入一个特定字符,点击发送按钮, RGB 彩色灯状态随之改变。 20.7 每课一问 1、串口 1 实验中发送的数据都是 8 位的,如果要发送的数据是 16 位的话,怎么办, 程序应该怎么修改? 答案如下 1 /***************** 发送一个 16 位数 **********************/ 2 void Usart_SendHalfWord( USART_TypeDef * pUSARTx, uint16_t ch) 3{ 4 uint8_t temp_h, temp_l; 5 6 /* 取出高八位 */ 7 temp_h = (ch&0XFF00)>>8; 8 /* 取出低八位 */ 9 temp_l = ch&0XFF; 10 11 /* 发送高八位 */ 12 USART_SendData(pUSARTx,temp_h); 13 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); 14 15 /* 发送低八位 */ 16 USART_SendData(pUSARTx,temp_l); 17 while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); 18 } 2、改写 USART1 指令控制 RGB 彩灯实验程序,把串口 1 换成串口 2。 第 187 页 共 928 零死角玩转 STM32—F429 第 188 页 共 928 零死角玩转 STM32—F429 第21章 DMA—直接存储区访问 本章参考资料:《STM32F4xx 中文参考手册》DMA 控制器章节。 学习本章时,配合《STM32F4xx 中文参考手册》DMA 控制器章节一起阅读,效果会 更佳,特别是涉及到寄存器说明的部分。本章内容专业名称较多,内容丰富也较难理解, 但非常有必要细读研究。 特别说明,本章内容是以 STM32F42xxx 系列资源讲解。 21.1 DMA 简介 DMA(Direct Memory Access,直接存储区访问)为实现数据高速在外设寄存器与存储器之 间或者存储器与存储器之间传输提供了高效的方法。之所以称之为高效,是因为 DMA 传 输实现高速数据移动过程无需任何 CPU 操作控制。从硬件层次上来说,DMA 控制器是独 立于 Cortex-M4 内核的,有点类似 GPIO、USART 外设一般,只是 DMA 的功能是可以快 速移动内存数据。 STM32F4xx 系列的 DMA 功能齐全,工作模式众多,适合不同编程环境要求。 STM32F4xx 系列的 DMA 支持外设到存储器传输、存储器到外设传输和存储器到存储器传 输三种传输模式。这里的外设一般指外设的数据寄存器,比如 ADC、SPI、I2C、DCMI 等 等外设的数据寄存器,存储器一般是指片内 SRAM、外部存储器、片内 Flash 等等。 外设到存储器传输就是把外设数据寄存器内容转移到指定的内存空间。比如进行 ADC 采集时我们可以利用 DMA 传输把 AD 转换数据转移到我们定义的存储区中,这样对于多 通道采集、采样频率高、连续输出数据的 AD 采集是非常高效的处理方法。 存储区到外设传输就是把特定存储区内容转移至外设的数据寄存器中,这种多用于外 设的发送通信。 存储器到存储器传输就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类 似于 C 语言内存拷贝函数 memcpy,利用 DMA 传输可以达到更高的传输效率,特别是 DMA 传输是不占用 CPU 的,可以节省很多 CPU 资源。 21.2 DMA 功能框图 STM32F4xx 系列的 DMA 可以实现外设寄存器与存储器之间或者存储器与存储器之间 传输三种模式,这要得益于 DMA 控制器是采样 AHB 主总线的,可以控制 AHB 总线矩阵 来启动 AHB 事务。图 21-1 为 DMA 控制器的框图。 第 189 页 共 928 零死角玩转 STM32—F429 图 21-1 DMA 框图 1. ①外设通道选择 STM32F4xx 系列资源丰富,具有两个 DMA 控制器,同时外设繁多,为实现正常传输, DMA 需要通道选择控制。每个 DMA 控制器具有 8 个数据流,每个数据流对应 8 个外设请 求。在实现 DMA 传输之前,DMA 控制器会通过 DMA 数据流 x 配置寄存器 DMA_SxCR(x 为 0~7,对应 8 个 DMA 数据流)的 CHSEL[2:0]位选择对应的通道作为该数据流的目标外设。 外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地 址。 DMA 请求映射情况参考表 21-1 和表 21-2。 表 21-1 DMA1 请求映射 外设请 求 数据流 0 数据流 1 数据流 2 数据流 3 数据流 4 数据流 5 数据流 6 数据流 7 通道 0 SPI3_RX SPI3_RX SPI2_RX SPI2_TX SPI3_TX SPI3_TX 通道 1 I2C1_RX 通道 2 TIM4_CH1 TIM7_UP TIM7_UP I2C1_RX I2C1_TX I2C1_TX I2S3_EXT_ I2S2_EXT_ I2S3_EXT_ TIM4_C TIM4_CH2 TIM4_UP RX TX TX H3 第 190 页 共 928 零死角玩转 STM32—F429 通道 3 通道 4 通道 5 通道 6 通道 7 I2S3_EXT_ RX UART5_R X UART8_T X TIM5_CH3 TIM5_UP TIM2_UP TIM2_CH 3 USART3_ RX UART7_T X TIM5_CH 4 TIM5_TRI G TIM6_UP I2C3_RX UART4_R X TIM3_CH4 TIM3_UP TIM5_CH1 I2C2_RX I2S2_EXT_ RX USART3_T X UART7_R X TIM5_CH4 TIM5_TRI G I2C2_RX I2C3_TX UART4_T X TIM3_CH1 TIM3_TRI G TIM5_CH2 USART3_ TX TIM2_CH1 USART2_ RX TIM3_CH2 DAC1 TIM2_CH 2 TIM2_CH 4 USART2_ TX UART8_R X TIM5_UP DAC2 TIM2_U P TIM2_C H4 UART5_ TX TIM3_C H3 I2C2_TX 表 21-2 DMA2 请求映射 外设 请求 通道 0 通道 1 数据流 0 ADC1 数据流 1 数据流 2 TIM8_CH1 TIM8_CH2 TIM8_CH3 数据流 3 数据流 4 ADC1 数据流 5 数据流 6 数据流 7 TIM1_CH1 TIM1_CH2 TIM1_CH3 DCMI ADC2 ADC2 SPI6_TX SPI6_RX DCMI 通道 2 ADC3 通道 3 SPI1_RX ADC3 SPI5_TX CRYP_OU SPI5_TX T CRYP_IN HASH_IN SPI1_RX SPI1_TX SPI1_TX 通道 4 SPI4_RX USART1_R SPI4_TX X SDIO USART1_ RX SDIO USART1_ TX 通道 5 USART6_ USART6_R SPI14_R SPI4_TX RX X X USART6_ USART6_ TX TX TIM1_CH 4 通道 6 TIM1_TRIG TIM1_CH TIM1_CH2 1 TIM1_C TIM1_CO H1 M TIM1_UP TIM1_CH3 TIM1_TR IG TIM8_CH4 通道 7 TIM8_UP TIM8_CH1 TIM8_C TIM8_CH H2 3 SPI5_RX SPI5_TX TIM8_TRI G TIM8_CO M 每个外设请求都占用一个数据流通道,相同外设请求可以占用不同数据流通道。比如 SPI3_RX 请求,即 SPI3 数据发送请求,占用 DMA1 的数据流 0 的通道 0,因此当我们使用 该请求时,我们需要在把 DMA_S0CR 寄存器的 CHSEL[2:0]设置为“000”,此时相同数据 流的其他通道不被选择,处于不可用状态,比如此时不能使用数据流 0 的通道 1 即 I2C1_RX 请求等等。 第 191 页 共 928 零死角玩转 STM32—F429 查阅表 21-1 可以发现 SPI3_RX 请求不仅仅在数据流 0 的通道 0,同时数据流 2 的通道 0 也是 SPI3_RX 请求,实际上其他外设基本上都有两个对应数据流通道,这两个数据流通 道都是可选的,这样设计是尽可能提供多个数据流同时使用情况选择。 2. ②仲裁器 一个 DMA 控制器对应 8 个数据流,数据流包含要传输数据的源地址、目标地址、数 据等等信息。如果我们需要同时使用同一个 DMA 控制器(DMA1 或 DMA2)多个外设请求 时,那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就 需要仲裁器来管理判断了。 仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时 可以通过寄存器设定它的优先级别,具体配置 DMA_SxCR 寄存器 PL[1:0]位,可以设置为 非常高、高、中和低四个级别。第二阶段属于硬件阶段,如果两个或以上数据流软件设置 优先级一样,则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流 2 优 先级高于数据流 3。 3. ③FIFO 每个数据流都独立拥有四级 32 位 FIFO(先进先出存储器缓冲区)。DMA 传输具有 FIFO 模式和直接模式。 直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果 DMA 配置 为存储器到外设传输那 DMA 会见一个数据存放在 FIFO 内,如果外设启动 DMA 传输请求 就可以马上将数据传输过去。 FIFO 用于在源数据传输到目标地址之前临时存放这些数据。可以通过 DMA 数据流 xFIFO 控制寄存器 DMA_SxFCR 的 FTH[1:0]位来控制 FIFO 的阈值,分别为 1/4、1/2、3/4 和满。如果数据存储量达到阈值级别时,FIFO 内容将传输到目标中。 FIFO 对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的 字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来 4 个 8 位 字节的数据拼凑成一个 32 位字数据。此时使用 FIFO 功能先把数据缓存起来,分别根据需 要输出数据。 FIFO 另外一个作用使用于突发(burst)传输。 4. ④存储器端口、⑤外设端口 DMA 控制器实现双 AHB 主接口,更好利用总线矩阵和并行传输。DMA 控制器通过 存储器端口和外设端口与存储器和外设进行数据传输,关系见图 21-2。DMA 控制器的功 能是快速转移内存数据,需要一个连接至源数据地址的端口和一个连接至目标地址的端口。 DMA2(DMA 控制器 2)的存储器端口和外设端口都是连接到 AHB 总线矩阵,可以使用 AHB 总线矩阵功能。DMA2 存储器和外设端口可以访问相关的内存地址,包括有内部 Flash、内部 SRAM、AHB1 外设、AHB2 外设、APB2 外设和外部存储器空间。 第 192 页 共 928 零死角玩转 STM32—F429 DMA1 的存储区端口相比 DMA2 的要减少 AHB2 外设的访问权,同时 DMA1 外设端 口是没有连接至总线矩阵的,只有连接到 APB1 外设,所以 DMA1 不能实现存储器到存储 器传输。 图 21-2 两个 DMA 控制器系统实现 5. ⑥编程端口 AHB 从器件编程端口是连接至 AHB2 外设的。AHB2 外设在使用 DMA 传输时需要相 关控制信号。 21.3 DMA 数据配置 DMA 工作模式多样,具有多种可能工作模式,具体可能配置见表 21-3。 表 21-3 DMA 配置可能情况 DMA 传输模式 源 外设 AHB 目标 AHB 流控制器 DMA 循环模式 允许 传输类型 单次 直接模式 双缓冲模式 允许 允许 第 193 页 共 928 零死角玩转 STM32—F429 到存储器 外设端口 存储器端口 外设 存储器 到外设 AHB 存储器端口 AHB 外设端口 DMA 外设 存储器 到存储器 AHB AHB 存储器端口 存储器端口 仅 DMA 禁止 允许 禁止 禁止 突发 单次 突发 单次 突发 单次 突发 单次 突发 禁止 允许 禁止 允许 禁止 允许 禁止 禁止 禁止 允许 禁止 禁止 1. DMA 传输模式 DMA2 支持全部三种传输模式,而 DMA1 只有外设到存储器和存储器到外设两种模式。 模式选择可以通过 DMA_SxCR 寄存器的 DIR[1:0]位控制,进而将 DMA_SxCR 寄存器的 EN 位置 1 就可以使能 DMA 传输。 在 DMA_SxCR 寄存器的 PSIZE[1:0]和 MSIZE[1:0]位分别指定外设和存储器数据宽度 大小,可以指定为字节(8 位)、半字(16 位)和字(32 位),我们可以根据实际情况设置。直接 模式要求外设和存储器数据宽度大小一样,实际上在这种模式下 DMA 数据流直接使用 PSIZE,MSIZE 不被使用。 2. 源地址和目标地址 DMA 数据流 x 外设地址 DMA_SxPAR(x 为 0~7)寄存器用来指定外设地址,它是一个 32 位数据有效寄存器。DMA 数据流 x 存储器 0 地址 DMA_SxM0AR(x 为 0~7) 寄存器和 DMA 数据流 x 存储器 1 地址 DMA_SxM1AR(x 为 0~7) 寄存器用来存放存储器地址,其中 DMA_SxM1AR 只用于双缓冲模式,DMA_SxM0AR 和 DMA_SxM1AR 都是 32 位数据有效 的。 当选择外设到存储器模式时,即设置 DMA_SxCR 寄存器的 DIR[1:0] 位为“00”, DMA_SxPAR 寄存器为外设地址,也是传输的源地址,DMA_SxM0AR 寄存器为存储器地 址,也是传输的目标地址。对于存储器到存储器传输模式,即设置 DIR[1:0] 位为“10”时, 采用与外设到存储器模式相同配置。而对于存储器到外设,即设置 DIR[1:0]位为“01”时, DMA_SxM0AR 寄存器作为为源地址,DMA_SxPAR 寄存器作为目标地址。 3. 流控制器 流控制器主要涉及到一个控制 DMA 传输停止问题。DMA 传输在 DMA_SxCR 寄存器 的 EN 位被置 1 后就进入准备传输状态,如果有外设请求 DMA 传输就可以进行数据传输。 很多情况下,我们明确知道传输数据的数目,比如要传 1000 个或者 2000 个数据,这样我 们就可以在传输之前设置 DMA_SxNDTR 寄存器为要传输数目值,DMA 控制器在传输完 这么多数目数据后就可以控制 DMA 停止传输。 DMA 数据流 x 数据项数 DMA_SxNDTR(x 为 0~7)寄存器用来记录当前仍需要传输数目, 它是一个 16 位数据有效寄存器,即最大值为 65535,这个值在程序设计是非常有用也是需 第 194 页 共 928 零死角玩转 STM32—F429 要注意的地方。我们在编程时一般都会明确指定一个传输数量,在完成一次数目传输后 DMA_SxNDTR 计数值就会自减,当达到零时就说明传输完成。 如果某些情况下在传输之前我们无法确定数据的数目,那 DMA 就无法自动控制传输 停止了,此时需要外设通过硬件通信向 DMA 控制器发送停止传输信号。这里有一个大前 提就是外设必须是可以发出这个停止传输信号,只有 SDIO 才有这个功能,其他外设不具 备此功能。 4. 循环模式 循环模式相对应于一次模式。一次模式就是传输一次就停止传输,下一次传输需要手 动控制,而循环模式在传输一次后会自动按照相同配置重新传输,周而复始直至被控制停 止或传输发生错误。 通过 DMA_SxCR 寄存器的 CIRC 位可以使能循环模式。 5. 传输类型 DMA 传输类型有单次(Single)传输和突发(Burst)传输。突发传输就是用非常短时间结 合非常高数据信号率传输数据,相对正常传输速度,突发传输就是在传输阶段把速度瞬间 提高,实现高速传输,在数据传输完成后恢复正常速度,有点类似达到数据块“秒传”效 果。为达到这个效果突发传输过程要占用 AHB 总线,保证要求每个数据项在传输过程不 被分割,这样一次性把数据全部传输完才释放 AHB 总线;而单次传输时必须通过 AHB 的 总线仲裁多次控制才传输完成。 单次和突发传输数据使用具体情况参考表 21-4。其中 PBURST[1:0]和 MBURST[1:0]位 是位于 DMA_SxCR 寄存器中的,用于分别设置外设和存储器不同节拍数的突发传输,对 应为单次传输、4 个节拍增量传输、8 个节拍增量传输和 16 个节拍增量传输。PINC 位和 MINC 位是寄存器 DMA_SxCR 寄存器的第 9 和第 10 位,如果位被置 1 则在每次数据传输 后数据地址指针自动递增,其增量由 PSIZE 和 MSIZE 值决定,比如,设置 PSIZE 为半字 大小,那么下一次传输地址将是前一次地址递增 2。 表 21-4 DMA 传输类型 AHB 主端口 项目 单次传输 寄存器 PBURST[1:0]=00,PINC 无要求 突发传输 PBURST[1:0]不为 0,PINC 必须为 1 外设 描述 每次 DMA 请求就传输一次字节/半字/ 字(取决于 PSIZE)数据 每次 DMA 请求就传输 4/8/16 个(取决 于 PBURST[1:0])字节/半字/字(取决 于 PSIZE)数据 寄存器 MBURST[1:0]=00,MINC 无要求 MBURST[1:0]不为 0,MINC 必须为 1 存储器 描述 每次 DMA 请求就传输一次字节/半字/ 字(取决于 MSIZE)数据 每次 DMA 请求就传输 4/8/16 个(取决 于 MBURST[1:0])字节/半字/字(取决 于 MSIZE)数据 第 195 页 共 928 零死角玩转 STM32—F429 突发传输与 FIFO 密切相关,突发传输需要结合 FIFO 使用,具体要求 FIFO 阈值一定 要是内存突发传输数据量的整数倍。FIFO 阈值选择和存储器突发大小必须配合使用,具体 参考表 21-5。 表 21-5 FIFO 阈值配置 MSIZE 字节 半字 字 FIFO 级别 1/4 1/2 3/4 满 1/4 1/2 3/4 满 1/4 1/2 3/4 满 MBURST=INCR4 4 个节拍的 1 次突发 4 个节拍的 2 次突发 4 个节拍的 3 次突发 4 个节拍的 4 次突发 禁止 4 个节拍的 1 次突发 禁止 4 个节拍的 2 次突发 禁止 4 个节拍的 1 次突发 MBURST=INCR8 禁止 8 个节拍的 1 次突发 禁止 8 个节拍的 2 次突发 禁止 8 个节拍的 1 次突发 禁止 MBURST=INCR16 禁止 16 个节拍的 1 次突发 禁止 6. 直接模式 默认情况下,DMA 工作在直接模式,不使能 FIFO 阈值级别。 直接模式在每个外设请求都立即启动对存储器传输的单次传输。直接模式要求源地址 和目标地址的数据宽度必须一致,所以只有 PSIZE 控制,而 MSIZE 值被忽略。突发传输 是基于 FIFO 的所以直接模式不被支持。另外直接模式不能用于存储器到存储器传输。 在直接模式下,如果 DMA 配置为存储器到外设传输那 DMA 会见一个数据存放在 FIFO 内,如果外设启动 DMA 传输请求就可以马上将数据传输过去。 7. 双缓冲模式 设置 DMA_SxCR 寄存器的 DBM 位为 1 可启动双缓冲传输模式,并自动激活循环模式。 双缓冲不应用与存储器到存储器的传输。双缓冲模式下,两个存储器地址指针都有效,即 DMA_SxM1AR 寄存器将被激活使用。开始传输使用 DMA_SxM0AR 寄存器的地址指针所 对应的存储区,当这个存储区数据传输完 DMA 控制器会自动切换至 DMA_SxM1AR 寄存 器的地址指针所对应的另一块存储区,如果这一块也传输完成就再切换至 DMA_SxM0AR 寄存器的地址指针所对应的存储区,这样循环调用。 当其中一个存储区传输完成时都会把传输完成中断标志 TCIF 位置 1,如果我们使能了 DMA_SxCR 寄存器的传输完成中断,则可以产生中断信号,这个对我们编程非常有用。另 外一个非常有用的信息是 DMA_SxCR 寄存器的 CT 位,当 DMA 控制器是在访问使用 DMA_SxM0AR 时 CT=0,此时 CPU 不能访问 DMA_SxM0AR,但可以向 DMA_SxM1AR 填充或者读取数据;当 DMA 控制器是在访问使用 DMA_SxM1AR 时 CT=1,此时 CPU 不 能访问 DMA_SxM1AR,但可以向 DMA_SxM0AR 填充或者读取数据。另外在未使能 DMA 数据流传输时,可以直接写 CT 位,改变开始传输的目标存储区。 第 196 页 共 928 零死角玩转 STM32—F429 双缓冲模式应用在需要解码程序的地方是非常有效的。比如 MP3 格式音频解码播放, MP3 是被压缩的文件格式,我们需要特定的解码库程序来解码文件才能得到可以播放的 PCM 信号,解码需要一定的实际,按照常规方法是读取一段原始数据到缓冲区,然后对缓 冲区内容进行解码,解码后才输出到音频播放电路,这种流程对 CPU 运算速度要求高,很 容易出现播放不流畅现象。如果我们使用 DMA 双缓冲模式传输数据就可以非常好的解决 这个问题,达到解码和输出音频数据到音频电路同步进行的效果。 8. DMA 中断 每个 DMA 数据流可以在发送以下事件时产生中断: 1) 达到半传输:DMA 数据传输达到一半时 HTIF 标志位被置 1,如果使能 HTIE 中 断控制位将产生达到半传输中断; 2) 传输完成:DMA 数据传输完成时 TCIF 标志位被置 1,如果使能 TCIE 中断控制 位将产生传输完成中断; 3) 传输错误:DMA 访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器 地址寄存器时 TEIF 标志位被置 1,如果使能 TEIE 中断控制位将产生传输错误中 断; 4) FIFO 错误:发生 FIFO 下溢或者上溢时 FEIF 标志位被置 1,如果使能 FEIE 中断 控制位将产生 FIFO 错误中断; 5) 直接模式错误:在外设到存储器的直接模式下,因为存储器总线没得到授权,使 得先前数据没有完成被传输到存储器空间上,此时 DMEIF 标志位被置 1,如果使 能 DMEIE 中断控制位将产生直接模式错误中断。 21.4 DMA 初始化结构体详解 标准库函数对每个外设都建立了一个初始化结构体 xxx_InitTypeDef(xxx 为外设名称), 结构体成员用于设置外设工作参数,并由标准库函数 xxx_Init()调用这些设定参数进入设置 外设相应的寄存器,达到配置外设工作环境的目的。 结构体 xxx_InitTypeDef 和库函数 xxx_Init 配合使用是标准库精髓所在,理解了结构体 xxx_InitTypeDef 每个成员意义基本上就可以对该外设运用自如了。结构体 xxx_InitTypeDef 定义在 stm32f4xx_xxx.h(后面 xxx 为外设名称)文件中,库函数 xxx_Init 定义在 stm32f4xx_xxx.c 文件中,编程时我们可以结合这两个文件内注释使用。 DMA_ InitTypeDef 初始化结构体 1 typedef struct { 2 uint32_t DMA_Channel; 3 uint32_t DMA_PeripheralBaseAddr; 4 uint32_t DMA_Memory0BaseAddr; 5 uint32_t DMA_DIR; 6 uint32_t DMA_BufferSize; //通道选择 //外设地址 //存储器 0 地址 //传输方向 //数据数目 第 197 页 共 928 零死角玩转 STM32—F429 7 uint32_t DMA_PeripheralInc; 8 uint32_t DMA_MemoryInc; 9 uint32_t DMA_PeripheralDataSize; 10 uint32_t DMA_MemoryDataSize; 11 uint32_t DMA_Mode; 12 uint32_t DMA_Priority; 13 uint32_t DMA_FIFOMode; 14 uint32_t DMA_FIFOThreshold; 15 uint32_t DMA_MemoryBurst; 16 uint32_t DMA_PeripheralBurst; 17 } DMA_InitTypeDef; //外设递增 //存储器递增 //外设数据宽度 //存储器数据宽度 //模式选择 //优先级 //FIFO 模式 //FIFO 阈值 //存储器突发传输 //外设突发传输 5) DMA_Channel:DMA 请求通道选择,可选通道 0 至通道 7,每个外设对应固定的 通道,具 体设置值需要查 表 21-1 和表 21-2 ;它设 定 DMA_SxCR 寄存器 的 CHSEL[2:0]位的值。例如,我们使用模拟数字转换器 ADC3 规则采集 4 个输入通 道的电压数据,查表 21-2 可知使用通道 2。 6) DMA_PeripheralBaseAddr:外设地址,设定 DMA_SxPAR 寄存器的值;一般设置 为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储区 地址。ADC3 的数据寄存器 ADC_DR 地址为((uint32_t)ADC3+0x4C)。 7) DMA_Memory0BaseAddr:存储器 0 地址,设定 DMA_SxM0AR 寄存器值;一般 设置为我们自定义存储区的首地址。我们程序先自定义一个 16 位无符号整形数组 ADC_ConvertedValue[4]用来存放每个通道的 ADC 值,所以把数组首地址(直接使 用数组名即可)赋值给 DMA_Memory0BaseAddr。 8) DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存 储器。它设定 DMA_SxCR 寄存器的 DIR[1:0]位的值。ADC 采集显然使用外设到 存储器模式。 9) DMA_BufferSize:设定待传输数据数目,初始化设定 DMA_SxNDTR 寄存器的值。 这里 ADC 是采集 4 个通道数据,所以待传输数目也就是 4。 10) DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动 递增功能,它设定 DMA_SxCR 寄存器的 PINC 位的值;一般外设都是只有一个数 据寄存器,所以一般不会使能该位。ADC3 的数据寄存器地址是固定并且只有一 个所以不使能外设地址递增。 11) DMA_MemoryInc:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动 递增功能,它设定 DMA_SxCR 寄存器的 MINC 位的值;我们自定义的存储区一 般都是存放多个数据的,所以使能存储器地址自动递增功能。我们之前已经定义 了一个包含 4 个元素的数字用来存放数据,使能存储区地址递增功能,自动把每 个通道数据存放到对应数组元素内。 第 198 页 共 928 零死角玩转 STM32—F429 12) DMA_PeripheralDataSize:外设数据宽度,可选字节(8 位)、半字(16 位)和字(32 位),它设定 DMA_SxCR 寄存器的 PSIZE[1:0]位的值。ADC 数据寄存器只有低 16 位数据有效,使用半字数据宽度。 13) DMA_MemoryDataSize:存储器数据宽度,可选字节(8 位)、半字(16 位)和字(32 位),它设定 DMA_SxCR 寄存器的 MSIZE[1:0]位的值。保存 ADC 转换数据也要 使用半字数据宽度,这跟我们定义的数组是相对应的。 14) DMA_Mode : DMA 传 输 模 式 选 择 , 可 选 一 次 传 输 或 者 循 环 传 输 , 它 设 定 DMA_SxCR 寄存器的 CIRC 位的值。我们希望 ADC 采集是持续循环进行的,所 以使用循环传输模式。 15) DMA_Priority:软件设置数据流的优先级,有 4 个可选优先级分别为非常高、高、 中和低,它设定 DMA_SxCR 寄存器的 PL[1:0]位的值。DMA 优先级只有在多个 DMA 数据流同时使用时才有意义,这里我们设置为非常高优先级就可以了。 16) DMA_FIFOMode:FIFO 模式使能,如果设置为 DMA_FIFOMode_Enable 表示使 能 FIFO 模式功能;它设定 DMA_SxFCR 寄存器的 DMDIS 位。ADC 采集传输使 用直接传输模式即可,不需要使用 FIFO 模式。 17) DMA_FIFOThreshold:FIFO 阈值选择,可选 4 种状态分别为 FIFO 容量的 1/4、 1/2、3/4 和满;它设定 DMA_SxFCR 寄存器的 FTH[1:0]位; DMA_FIFOMode 设 置为 DMA_FIFOMode_Disable,那 DMA_FIFOThreshold 值无效。ADC 采集传输 不使用 FIFO 模式,设置改值无效。 18) DMA_MemoryBurst:存储器突发模式选择,可选单次模式、4 节拍的增量突发模 式、8 节拍的增量突发模式或 16 节拍的增量突发模式,它设定 DMA_SxCR 寄存 器的 MBURST[1:0]位的值。ADC 采集传输是直接模式,要求使用单次模式。 19) DMA_PeripheralBurst:外设突发模式选择,可选单次模式、4 节拍的增量突发模 式、8 节拍的增量突发模式或 16 节拍的增量突发模式,它设定 DMA_SxCR 寄存 器的 PBURST[1:0]位的值。ADC 采集传输是直接模式,要求使用单次模式。 21.5 DMA 存储器到存储器模式实验 DMA 工作模式多样,具体如何使用需要配合实际传输条件具体分析。接下来我们通过 两个实验详细讲解 DMA 不同模式下的使用配置,加深我们对 DMA 功能的理解。 DMA 运行高效,使用方便,在很多测试实验都会用到,这里先详解存储器到存储器和 存储器到外设这两种模式,其他功能模式在其他章节会有很多使用到的情况,也会有相关 的分析。 第 199 页 共 928 零死角玩转 STM32—F429 存储器到存储器模式可以实现数据在两个内存的快速拷贝。我们先定义一个静态的源 数据,然后使用 DMA 传输把源数据拷贝到目标地址上,最后对比源数据和目标地址的数 据,看看是否传输准确。 21.5.1 硬件设计 DMA 存储器到存储器实验不需要其他硬件要求,只用到 RGB 彩色灯用于指示程序状 态,关于 RGB 彩色灯电路可以参考 GPIO 章节。 21.5.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。这个实验代码比较简单,主要程序代码都在 main.c 文件中。 1. 编程要点 1) 使能 DMA 数据流时钟并复位初始化 DMA 数据流; 2) 配置 DMA 数据流参数; 3) 使能 DMA 数据流,进行传输; 4) 等待传输完成,并对源数据和目标地址数据进行比较。 2. 代码分析 DMA 宏定义及相关变量定义 代码清单 21-1 DMA 数据流和相关变量定义 1 /* 相关宏定义,使用存储器到存储器传输必须使用 DMA2 */ 2 #define DMA_STREAM DMA2_Stream0 3 #define DMA_CHANNEL DMA_Channel_0 4 #define DMA_STREAM_CLOCK RCC_AHB1Periph_DMA2 5 #define DMA_FLAG_TCIF DMA_FLAG_TCIF0 6 7 #define BUFFER_SIZE 32 8 #define TIMEOUT_MAX 10000 /* Maximum timeout value */ 9 10 /* 定义 aSRC_Const_Buffer 数组作为 DMA 传输数据源 11 const 关键字将 aSRC_Const_Buffer 数组变量定义为常量类型 */ 12 const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= { 13 0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10, 14 0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20, 15 0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30, 16 0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40, 17 0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50, 18 0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60, 19 0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70, 20 0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80 第 200 页 共 928 零死角玩转 STM32—F429 21 }; 22 /* 定义 DMA 传输目标存储器 */ 23 uint32_t aDST_Buffer[BUFFER_SIZE]; 使用宏定义设置外设配置方便程序修改和升级。 存储器到存储器传输必须使用 DMA2,但对数据流编号以及通道选择就没有硬性要求, 可以自由选择。 aSRC_Const_Buffer[BUFFER_SIZE]是定义用来存放源数据的,并且使用了 const 关键 字修饰,即常量类型,使得变量是存储在内部 flash 空间上。 DMA 数据流配置 代码清单 21-2 DMA 传输参数配置 1 static void DMA_Config(void) 2{ 3 DMA_InitTypeDef DMA_InitStructure; 4 __IO uint32_t Timeout = TIMEOUT_MAX; 5 6 /* 使能 DMA 时钟 */ 7 RCC_AHB1PeriphClockCmd(DMA_STREAM_CLOCK, ENABLE); 8 9 /* 复位初始化 DMA 数据流 */ 10 DMA_DeInit(DMA_STREAM); 11 12 /* 确保 DMA 数据流复位完成 */ 13 while (DMA_GetCmdStatus(DMA_STREAM) != DISABLE) { 14 } 15 16 /* DMA 数据流通道选择 */ 17 DMA_InitStructure.DMA_Channel = DMA_CHANNEL; 18 /* 源数据地址 */ 19 DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)aSRC_Const_Buffer; 20 /* 目标地址 */ 21 DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)aDST_Buffer; 22 /* 存储器到存储器模式 */ 23 DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToMemory; 24 /* 数据数目 */ 25 DMA_InitStructure.DMA_BufferSize = (uint32_t)BUFFER_SIZE; 26 /* 使能自动递增功能 */ 27 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; 28 /* 使能自动递增功能 */ 29 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 30 /* 源数据是字大小(32 位) */ 31 DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Word; 32 /* 目标数据也是字大小(32 位) */ 33 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; 34 /* 一次传输模式,存储器到存储器模式不能使用循环传输 */ 35 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; 36 /* DMA 数据流优先级为高 */ 37 DMA_InitStructure.DMA_Priority = DMA_Priority_High; 38 /* 禁用 FIFO 模式 */ 39 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; 40 DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; 41 /* 单次模式 */ 42 DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; 43 /* 单次模式 */ 44 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; 45 /* 完成 DMA 数据流参数配置 */ 46 DMA_Init(DMA_STREAM, &DMA_InitStructure); 47 第 201 页 共 928 零死角玩转 STM32—F429 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 } /* 清除 DMA 数据流传输完成标志位 */ DMA_ClearFlag(DMA_STREAM,DMA_FLAG_TCIF); /* 使能 DMA 数据流,开始 DMA 数据传输 */ DMA_Cmd(DMA_STREAM, ENABLE); /* 检测 DMA 数据流是否有效并带有超时检测功能 */ Timeout = TIMEOUT_MAX; while ((DMA_GetCmdStatus(DMA_STREAM) != ENABLE) && (Timeout-- > 0)) { } /* 判断是否超时 */ if (Timeout == 0) { /* 超时就让程序运行下面循环:RGB 彩色灯闪烁 */ while (1) { LED_RED; Delay(0xFFFFFF); LED_RGBOFF; Delay(0xFFFFFF); } } 使用 DMA_InitTypeDef 结构体定义一个 DMA 数据流初始化变量,这个结构体内容我 们之前已经有详细讲解。定义一个无符号 32 位整数变量 Timeout 用来计数超时。 调用 RCC_AHB1PeriphClockCmd 函数开启 DMA 数据流时钟,使用 DMA 控制器之前 必须开启对应的时钟。 DMA_DeInit 函数见数据流复位到缺省配置状态。 使用 DMA_GetCmdStatus 函数获取当前 DMA 数据流状态,该函数接收一个 DMA 数 据流的参数,返回当前数据流状态,复位 DMA 数据流之前需要调用该函数来确保 DMA 数据流复位完成。 存储器到存储器模式通道选择没有具体规定,源地址和目标地址使用之前定义的数组 首地址,只能使用一次传输模式不能循环传输,最后我调用 DMA_Init 函数完成 DMA 数据 流的初始化配置。 DMA_ClearFlag 函数用于清除 DMA 数据流标志位,代码用到传输完成标志位,使用 之前清除标志位以免产生不必要干扰。DMA_ClearFlag 函数需要两个形参,一个是 DMA 数据流,一个是事件标志位,可选有数据流传输完成标志位、半传输标志位、FIFO 错误标 志位、传输错误标志位以及直接模式错误标志位。 DMA_Cmd 函数用于启动或者停止 DMA 数据流传输,它接收连个参数,第一个是 DMA 数据流,另外一个是开启 ENABLE 或者停止 DISABLE。 开启 DMA 传输后需要使用 DMA_GetCmdStatus 函数获取 DMA 数据流状态,确保 DMA 数据流配置有效,为防止程序卡死,添加了超时检测功能。 如果 DMA 配置超时错误闪烁 RGB 彩灯提示。 存储器数据对比 代码清单 21-3 源数据与目标地址数据对比 1 uint8_t Buffercmp(const uint32_t* pBuffer, 2 uint32_t* pBuffer1, uint16_t BufferLength) 3{ 第 202 页 共 928 零死角玩转 STM32—F429 4 5 6 7 8 9 10 11 12 13 14 15 16 17 } /* 数据长度递减 */ while (BufferLength--) { /* 判断两个数据源是否对应相等 */ if (*pBuffer != *pBuffer1) { /* 对应数据源不相等马上退出函数,并返回 0 */ return 0; } /* 递增两个数据源的地址指针 */ pBuffer++; pBuffer1++; } /* 完成判断并且对应数据相对 */ return 1; 判断指定长度的两个数据源是否完全相等,如果完全相等返回 1;只要其中一对数据 不相等返回 0。它需要三个形参,前两个是两个数据源的地址,第三个是要比较数据长度。 主函数 代码清单 21-4 存储器到存储器模式主函数 1 int main(void) 2{ 3 /* 定义存放比较结果变量 */ 4 uint8_t TransferStatus; 5 6 /* LED 端口初始化 */ 7 LED_GPIO_Config(); 8 9 /* 设置 RGB 彩色灯为紫色 */ 10 LED_PURPLE; 11 12 /* 简单延时函数 */ 13 Delay(0xFFFFFF); 14 15 /* DMA 传输配置 */ 16 DMA_Config(); 17 18 /* 等待 DMA 传输完成 */ 19 while (DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_TCIF)==DISABLE) { 20 21 } 22 23 /* 比较源数据与传输后数据 */ 24 TransferStatus=Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE); 25 26 /* 判断源数据与传输后数据比较结果*/ 27 if (TransferStatus==0) { 28 /* 源数据与传输后数据不相等时 RGB 彩色灯显示红色 */ 29 LED_RED; 30 } else { 31 /* 源数据与传输后数据相等时 RGB 彩色灯显示蓝色 */ 32 LED_BLUE; 33 } 34 35 while (1) { 36 } 37 } 首先定义一个变量用来保存存储器数据比较结果。 第 203 页 共 928 零死角玩转 STM32—F429 RGB 彩色灯用来指示程序进程,使用之前需要初始化它,LED_GPIO_Config 定义在 bsp_led.c 文件中。开始设置 RGB 彩色灯为紫色,LED_PURPLE 是定义在 bsp_led.h 文件的 一个宏定义。 Delay 函数只是一个简单的延时函数。 调用 DMA_Config 函数完成 DMA 数据流配置并启动 DMA 数据传输。 DMA_GetFlagStatus 函数获取 DMA 数据流事件标志位的当前状态,这里获取 DMA 数 据传输完成这个标志位,使用循环持续等待直到该标志位被置位,即 DMA 传输完成这个 事件发生,然后退出循环,运行之后程序。 确定 DMA 传输完成之后就可以调用 Buffercmp 函数比较源数据与 DMA 传输后目标地 址的数据是否一一对应。TransferStatus 保存比较结果,如果为 1 表示两个数据源一一对应 相等说明 DMA 传输成功;相反,如果为 0 表示两个数据源数据存在不等情况,说明 DMA 传输出错。 如果 DMA 传输成功设置 RGB 彩色灯为蓝色,如果 DMA 传输出错设置 RGB 彩色灯 为红色。 21.5.3 下载验证 确保开发板供电正常,编译程序并下载。观察 RGB 彩色灯变化情况。正常情况下 RGB 彩色灯先为紫色,然后变成蓝色。如果 DMA 传输出错才会为红色。 21.6 DMA 存储器到外设模式实验 DMA 存储器到外设传输模式非常方便把存储器数据传输外设数据寄存器中,这在 STM32 芯片向其他目标主机,比如电脑、另外一块开发板或者功能芯片,发送数据是非常 有用的。RS-232 串口通信是我们常用开发板与 PC 端通信的方法。我们可以使用 DMA 传 输把指定的存储器数据转移到 USART 数据寄存器内,并发送至 PC 端,在串口调试助手显 示。 21.6.1 硬件设计 存储器到外设模式使用到 USART1 功能,具体电路设置参考 USART 章节,无需其他 硬件设计。 21.6.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们编写两个串口驱动文件 bsp_usart_dma.c 和 bsp_usart_dma.h,有关串口和 DMA 的宏定义以及驱动函数都在里边。 1. 编程要点 1) 配置 USART 通信功能; 第 204 页 共 928 零死角玩转 STM32—F429 2) 设置 DMA 为存储器到外设模式,设置数据流通道,指定 USART 数据寄存器为目 标地址,循环发送模式; 3) 使能 DMA 数据流; 4) 使能 USART 的 DMA 发送请求; 5) DMA 传输同时 CPU 可以运行其他任务。 2. 代码分析 USART 和 DMA 宏定义 代码清单 21-5 USART 和 DMA 相关宏定义 1 //USART 2 #define DEBUG_USART 3 #define DEBUG_USART_CLK 4 #define DEBUG_USART_RX_GPIO_PORT 5 #define DEBUG_USART_RX_GPIO_CLK 6 #define DEBUG_USART_RX_PIN 7 #define DEBUG_USART_RX_AF 8 #define DEBUG_USART_RX_SOURCE 9 10 #define DEBUG_USART_TX_GPIO_PORT 11 #define DEBUG_USART_TX_GPIO_CLK 12 #define DEBUG_USART_TX_PIN 13 #define DEBUG_USART_TX_AF 14 #define DEBUG_USART_TX_SOURCE 15 16 #define DEBUG_USART_BAUDRATE 17 18 //DMA 19 #define DEBUG_USART_DR_BASE 20 #define SENDBUFF_SIZE 21 #define DEBUG_USART_DMA_CLK 22 #define DEBUG_USART_DMA_CHANNEL 23 #define DEBUG_USART_DMA_STREAM 使用宏定义设置外设配置方便程序修改和升级。 USART1 RCC_APB2Periph_USART1 GPIOA RCC_AHB1Periph_GPIOA GPIO_Pin_10 GPIO_AF_USART1 GPIO_PinSource10 GPIOA RCC_AHB1Periph_GPIOA GPIO_Pin_9 GPIO_AF_USART1 GPIO_PinSource9 115200 (USART1_BASE+0x04) 5000 //一次发送的数据量 RCC_AHB1Periph_DMA2 DMA_Channel_4 DMA2_Stream7 USART 部分设置与 USART 章节内容相同,可以参考 USART 章节内容理解。 查阅表 21-2 可知 USART1 对应 DMA2 的数据流 7 通道 4。 串口 DMA 传输配置 代码清单 21-6 USART1 发送请求 DMA 设置 1 void USART_DMA_Config(void) 2{ 3 DMA_InitTypeDef DMA_InitStructure; 4 5 /*开启 DMA 时钟*/ 6 RCC_AHB1PeriphClockCmd(DEBUG_USART_DMA_CLK, ENABLE); 7 8 /* 复位初始化 DMA 数据流 */ 9 DMA_DeInit(DEBUG_USART_DMA_STREAM); 10 第 205 页 共 928 零死角玩转 STM32—F429 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 } /* 确保 DMA 数据流复位完成 */ while (DMA_GetCmdStatus(DEBUG_USART_DMA_STREAM) != DISABLE) { } /*usart1 tx 对应 dma2,通道 4,数据流 7*/ DMA_InitStructure.DMA_Channel = DEBUG_USART_DMA_CHANNEL; /*设置 DMA 源:串口数据寄存器地址*/ DMA_InitStructure.DMA_PeripheralBaseAddr = DEBUG_USART_DR_BASE; /*内存地址(要传输的变量的指针)*/ DMA_InitStructure.DMA_Memory0BaseAddr = (u32)SendBuff; /*方向:从内存到外设*/ DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; /*传输大小 DMA_BufferSize=SENDBUFF_SIZE*/ DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; /*外设地址不增*/ DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; /*内存地址自增*/ DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; /*外设数据单位*/ DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte; /*内存数据单位 8bit*/ DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; /*DMA 模式:不断循环*/ DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; /*优先级:中*/ DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; /*禁用 FIFO*/ DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full; /*存储器突发传输 16 个节拍*/ DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; /*外设突发传输 1 个节拍*/ DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; /*配置 DMA2 的数据流 7*/ DMA_Init(DEBUG_USART_DMA_STREAM, &DMA_InitStructure); /*使能 DMA*/ DMA_Cmd(DEBUG_USART_DMA_STREAM, ENABLE); /* 等待 DMA 数据流有效*/ while (DMA_GetCmdStatus(DEBUG_USART_DMA_STREAM) != ENABLE) { } 使用 DMA_InitTypeDef 结构体定义一个 DMA 数据流初始化变量,这个结构体内容我 们之前已经有详细讲解。 调用 RCC_AHB1PeriphClockCmd 函数开启 DMA 数据流时钟,使用 DMA 控制器之前 必须开启对应的时钟。 DMA_DeInit 函数见数据流复位到缺省配置状态。 使用 DMA_GetCmdStatus 函数获取当前 DMA 数据流状态,该函数接收一个 DMA 数 据流的参数,返回当前数据流状态,复位 DMA 数据流之前需要调用该函数来确保 DMA 数据流复位完成。 USART 有固定的 DMA 通道,USART 数据寄存器地址也是固定的,外设地址不可以 使用自动递增,源数据使用我们自定义的数组空间,存储器地址使用自动递增,采用循环 发送模式,最后我调用 DMA_Init 函数完成 DMA 数据流的初始化配置。 第 206 页 共 928 零死角玩转 STM32—F429 DMA_Cmd 函数用于启动或者停止 DMA 数据流传输,它接收连个参数,第一个是 DMA 数据流,另外一个是开启 ENABLE 或者停止 DISABLE。 开启 DMA 传输后需要使用 DMA_GetCmdStatus 函数获取 DMA 数据流状态,确保 DMA 数据流配置有效。 主函数 代码清单 21-7 存储器到外设模式主函数 1 int main(void) 2{ 3 uint16_t i; 4 /* 初始化 USART */ 5 Debug_USART_Config(); 6 7 /* 配置使用 DMA 模式 */ 8 USART_DMA_Config(); 9 10 /* 配置 RGB 彩色灯 */ 11 LED_GPIO_Config(); 12 13 printf("\r\n USART1 DMA TX 测试 \r\n"); 14 15 /*填充将要发送的数据*/ 16 for (i=0; iSR1; 42 43 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(22); 44 } 45 /* 一直等待直到 addr 及 af 标志为 1 */ 46 while ((tmpSR1 & (I2C_SR1_ADDR | I2C_SR1_AF)) == 0); 47 48 /*检查 addr 标志是否为 1 */ 49 if (tmpSR1 & I2C_SR1_ADDR) 50 { 51 /* 清除 addr 标志该标志通过读 SR1 及 SR2 清除 */ 52 (void)EEPROM_I2C->SR2; 53 54 /*产生停止信号 */ 55 I2C_GenerateSTOP(EEPROM_I2C, ENABLE); 56 57 /* 退出函数 */ 58 return 1; 59 } 60 else 61 { 62 /*清除 af 标志 */ 63 I2C_ClearFlag(EEPROM_I2C, I2C_FLAG_AF); 第 232 页 共 928 零死角玩转 STM32—F429 64 } 65 66 /*检查等待次数*/ 67 if (EETrials++ == MAX_TRIAL_NUMBER) 68 { 69 /* 等待 MAX_TRIAL_NUMBER 次都还没准备好,退出等待 */ 70 return I2C_TIMEOUT_UserCallback(23); 71 } 72 } 73 } 这个函数主要实现是向 EEPROM 发送它设备地址,检测 EEPROM 的响应,若 EEPROM 接收到地址后返回应答信号,则表示 EEPROM 已经准备好,可以开始下一次通 讯。函数中检测响应是通过读取 STM32 的 SR1 寄存器的 ADDR 位及 AF 位来实现的,当 I2C 设备响应了地址的时候,ADDR 会置 1,若应答失败,AF 位会置 1。 EEPROM 的页写入 在以上的数据通讯中,每写入一个数据都需要向 EEPROM 发送写入的地址,我们希望 向连续地址写入多个数据的时候,只要告诉 EEPROM 第一个内存地址 address1,后面的数 据按次序写入到 address2、address3… 这样可以节省通讯的内容,加快速度。为应对这种 需求,EEPROM 定义了一种页写入时序,见图 23-15。 图 23-15 EEPROM 页写入时序(摘自《AT24C02》规格书) 根据页写入时序,第一个数据被解释为要写入的内存地址 address1,后续可连续发送 n 个 数据,这些数据会依次写入到内存中。其中 AT24C02 型号的芯片页写入时序最多可以一次 发送 8 个数据(即 n = 8 ),该值也称为页大小,某些型号的芯片每个页写入时序最多可传输 16 个数据。EEPROM 的页写入代码实现见代码清单 23-8。 代码清单 23-8 EEPROM 的页写入 1 2 /** 3 * @brief 在 EEPROM 的一个写循环中可以写多个字节,但一次写入的字节数 4* 不能超过 EEPROM 页的大小,AT24C02 每页有 8 个字节 5 * @param 6 * @param pBuffer:缓冲区指针 7 * @param WriteAddr:写地址 8 * @param NumByteToWrite:要写的字节数要求 NumByToWrite 小于页大小 9 * @retval 正常返回 1,异常返回 0 10 */ 11 uint8_t I2C_EE_PageWrite(uint8_t* pBuffer, uint8_t WriteAddr, 12 uint8_t NumByteToWrite) 13 { 14 I2CTimeout = I2CT_LONG_TIMEOUT; 15 第 233 页 共 928 零死角玩转 STM32—F429 16 while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY)) 17 { 18 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4); 19 } 20 21 /* 产生 I2C 起始信号 */ 22 I2C_GenerateSTART(EEPROM_I2C, ENABLE); 23 24 I2CTimeout = I2CT_FLAG_TIMEOUT; 25 26 /* 检测 EV5 事件并清除标志 */ 27 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 28 { 29 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5); 30 } 31 32 /* 发送 EEPROM 设备地址 */ 33 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter); 34 35 I2CTimeout = I2CT_FLAG_TIMEOUT; 36 37 /* 检测 EV6 事件并清除标志*/ 38 while (!I2C_CheckEvent(EEPROM_I2C, 39 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) 40 { 41 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6); 42 } 43 /* 发送要写入的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */ 44 I2C_SendData(EEPROM_I2C, WriteAddr); 45 46 I2CTimeout = I2CT_FLAG_TIMEOUT; 47 48 /* 检测 EV8 事件并清除标志*/ 49 while (! I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) 50 { 51 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7); 52 } 53 /* 循环发送 NumByteToWrite 个数据 */ 54 while (NumByteToWrite--) 55 { 56 /* 发送缓冲区中的数据 */ 57 I2C_SendData(EEPROM_I2C, *pBuffer); 58 59 /* 指向缓冲区中的下一个数据 */ 60 pBuffer++; 61 62 I2CTimeout = I2CT_FLAG_TIMEOUT; 63 64 /* 检测 EV8 事件并清除标志*/ 65 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) 66 { 67 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8); 68 } 69 } 70 /* 发送停止信号 */ 71 I2C_GenerateSTOP(EEPROM_I2C, ENABLE); 72 return 1; 73 } 这段页写入函数主体跟单字节写入函数是一样的,只是它在发送数据的时候,使用 for 循环控制发送多个数据,发送完多个数据后才产生 I2C 停止信号,只要每次传输的数据小 于等于 EEPROM 时序规定的页大小,就能正常传输。 第 234 页 共 928 零死角玩转 STM32—F429 快速写入多字节 利用 EEPROM 的页写入方式,可以改进前面的“多字节写入”函数,加快传输速度, 见代码清单 23-9。 代码清单 23-9 快速写入多字节函数 1 2 /* AT24C01/02 每页有 8 个字节 */ 3 #define I2C_PageSize 8 4 5 /** 6 * @brief 将缓冲区中的数据写到 I2C EEPROM 中,采用页写入的方式,加快写入速度 7 * @param pBuffer:缓冲区指针 8 * @param WriteAddr:写地址 9 * @param NumByteToWrite:写的字节数 10 * @retval 无 11 */ 12 void I2C_EE_BufferWrite(uint8_t* pBuffer, uint8_t WriteAddr, 13 u16 NumByteToWrite) 14 { 15 uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0; 16 17 /*mod 运算求余,若 writeAddr 是 I2C_PageSize 整数倍,运算结果 Addr 值为 0*/ 18 Addr = WriteAddr % I2C_PageSize; 19 20 /*差 count 个数据,刚好可以对齐到页地址*/ 21 count = I2C_PageSize - Addr; 22 /*计算出要写多少整数页*/ 23 NumOfPage = NumByteToWrite / I2C_PageSize; 24 /*mod 运算求余,计算出剩余不满一页的字节数*/ 25 NumOfSingle = NumByteToWrite % I2C_PageSize; 26 27 /* Addr=0,则 WriteAddr 刚好按页对齐 aligned */ 28 if (Addr == 0) 29 { 30 /* 如果 NumByteToWrite < I2C_PageSize */ 31 if (NumOfPage == 0) 32 { 33 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 34 I2C_EE_WaitEepromStandbyState(); 35 } 36 /* 如果 NumByteToWrite > I2C_PageSize */ 37 else 38 { 39 /*先把整数页都写了*/ 40 while (NumOfPage--) 41 { 42 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize); 43 I2C_EE_WaitEepromStandbyState(); 44 WriteAddr += I2C_PageSize; 45 pBuffer += I2C_PageSize; 46 } 47 48 /*若有多余的不满一页的数据,把它写完*/ 49 if (NumOfSingle!=0) 50 { 51 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 52 I2C_EE_WaitEepromStandbyState(); 53 } 54 } 55 } 56 /* 如果 WriteAddr 不是按 I2C_PageSize 对齐 */ 57 else 第 235 页 共 928 零死角玩转 STM32—F429 58 { 59 /* 如果 NumByteToWrite < I2C_PageSize */ 60 if (NumOfPage== 0) 61 { 62 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 63 I2C_EE_WaitEepromStandbyState(); 64 } 65 /* 如果 NumByteToWrite > I2C_PageSize */ 66 else 67 { 68 /*地址不对齐多出的 count 分开处理,不加入这个运算*/ 69 NumByteToWrite -= count; 70 NumOfPage = NumByteToWrite / I2C_PageSize; 71 NumOfSingle = NumByteToWrite % I2C_PageSize; 72 73 /*先把 WriteAddr 所在页的剩余字节写了*/ 74 if (count != 0) 75 { 76 I2C_EE_PageWrite(pBuffer, WriteAddr, count); 77 I2C_EE_WaitEepromStandbyState(); 78 79 /*WriteAddr 加上 count 后,地址就对齐到页了*/ 80 WriteAddr += count; 81 pBuffer += count; 82 } 83 /*把整数页都写了*/ 84 while (NumOfPage--) 85 { 86 I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize); 87 I2C_EE_WaitEepromStandbyState(); 88 WriteAddr += I2C_PageSize; 89 pBuffer += I2C_PageSize; 90 } 91 /*若有多余的不满一页的数据,把它写完*/ 92 if (NumOfSingle != 0) 93 { 94 I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle); 95 I2C_EE_WaitEepromStandbyState(); 96 } 97 } 98 } 99 } 很多读者觉得这段代码的运算很复杂,看不懂,其实它的主旨就是对输入的数据进行 分页(本型号芯片每页 8 个字节),见表 23-2。通过“整除”计算要写入的数据 NumByteToWrite 能写满多少“完整的页”,计算得的值存储在 NumOfPage 中,但有时数 据不是刚好能写满完整页的,会多一点出来,通过“求余”计算得出“不满一页的数据个 数”就存储在 NumOfSingle 中。计算后通过按页传输 NumOfPage 次整页数据及最后的 NumOfSing 个数据,使用页传输,比之前的单个字节数据传输要快很多。 除了基本的分页传输,还要考虑首地址的问题,见表 23-3。若首地址不是刚好对齐到 页的首地址,会需要一个 count 值,用于存储从该首地址开始写满该地址所在的页,还能 写多少个数据。实际传输时,先把这部分 count 个数据先写入,填满该页,然后把剩余的 数据(NumByteToWrite-count),再重复上述求出 NumOPage 及 NumOfSingle 的过程,按页 传输到 EEPROM。 1. 若 writeAddress=16,计算得 Addr=16%8= 0 ,count=8-0= 8; 2. 同时,若 NumOfPage=22,计算得 NumOfPage=22/8= 2,NumOfSingle=22%8= 6。 第 236 页 共 928 零死角玩转 STM32—F429 3. 数据传输情况如表 23-2 表 23-2 首地址对齐到页时的情况 不影响 0 1 2 3 4 5 6 7 不影响 8 9 10 11 12 13 14 15 第1页 16 17 18 19 20 21 22 23 第2页 24 25 26 27 28 29 30 31 NumOfSingle=6 32 33 34 35 36 37 38 39 4. 若 writeAddress=17,计算得 Addr=17%8= 1,count=8-1= 7; 5. 同时,若 NumOfPage=22, 6. 先把 count 去掉,特殊处理,计算得新的 NumOfPage=22-7= 15 7. 计算得 NumOfPage=15/8= 1,NumOfSingle=15%8= 7。 8. 数据传输情况如表 23-3 表 23-3 首地址未对齐到页时的情况 不影响 0 1 2 3 4 5 6 7 不影响 8 9 10 11 12 13 14 15 count=7 16 17 18 19 20 21 22 23 第1页 24 25 26 27 28 29 30 31 NumOfSingle=7 32 33 34 35 36 37 38 39 最后,强调一下,EEPROM 支持的页写入只是一种加速的 I2C 的传输时序,实际上并 不要求每次都以页为单位进行读写,EEPROM 是支持随机访问的(直接读写任意一个地址), 如前面的单个字节写入。在某些存储器,如 NAND FLASH,它是必须按照 Block 写入的, 例如每个 Block 为 512 或 4096 字节,数据写入的最小单位是 Block,写入前都需要擦除整 个 Block;NOR FLASH 则是写入前必须以 Sector/Block 为单位擦除,然后才可以按字节写 入。而我们的 EEPROM 数据写入和擦除的最小单位是“字节”而不是“页”,数据写入前 不需要擦除整页。 从 EEPROM 读取数据 从 EEPROM 读取数据是一个复合的 I2C 时序,它实际上包含一个写过程和一个读过 程,见图 23-16。 第 237 页 共 928 零死角玩转 STM32—F429 图 23-16 EEPROM 数据读取时序 读时序的第一个通讯过程中,使用 I2C 发送设备地址寻址(写方向),接着发送要读取 的“内存地址”;第二个通讯过程中,再次使用 I2C 发送设备地址寻址,但这个时候的数 据方向是读方向;在这个过程之后,EEPROM 会向主机返回从“内存地址”开始的数据, 一个字节一个字节地传输,只要主机的响应为“应答信号”,它就会一直传输下去,主机 想结束传输时,就发送“非应答信号”,并以“停止信号”结束通讯,作为从机的 EEPROM 也会停止传输。实现代码见代码清单 23-10。 代码清单 23-10 从 EEPROM 读取数据 1 2 /** 3 * @brief 从 EEPROM 里面读取一块数据 4 * @param pBuffer:存放从 EEPROM 读取的数据的缓冲区指针 5 * @param ReadAddr:接收数据的 EEPROM 的地址 6 * @param NumByteToRead:要从 EEPROM 读取的字节数 7 * @retval 正常返回 1,异常返回 0 8 */ 9 uint8_t I2C_EE_BufferRead(uint8_t* pBuffer, uint8_t ReadAddr, 10 u16 NumByteToRead) 11 { 12 I2CTimeout = I2CT_LONG_TIMEOUT; 13 14 while (I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY)) 15 { 16 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9); 17 } 18 19 /* 产生 I2C 起始信号 */ 20 I2C_GenerateSTART(EEPROM_I2C, ENABLE); 21 22 I2CTimeout = I2CT_FLAG_TIMEOUT; 23 第 238 页 共 928 零死角玩转 STM32—F429 24 /* 检测 EV5 事件并清除标志*/ 25 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 26 { 27 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10); 28 } 29 30 /* 发送 EEPROM 设备地址 */ 31 I2C_Send7bitAddress(EEPROM_I2C,EEPROM_ADDRESS,I2C_Direction_Transmitter); 32 33 I2CTimeout = I2CT_FLAG_TIMEOUT; 34 35 /* 检测 EV6 事件并清除标志*/ 36 while (!I2C_CheckEvent(EEPROM_I2C, 37 I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) 38 { 39 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11); 40 } 41 /*通过重新设置 PE 位清除 EV6 事件 */ 42 I2C_Cmd(EEPROM_I2C, ENABLE); 43 44 /* 发送要读取的 EEPROM 内部地址(即 EEPROM 内部存储器的地址) */ 45 I2C_SendData(EEPROM_I2C, ReadAddr); 46 47 I2CTimeout = I2CT_FLAG_TIMEOUT; 48 49 /* 检测 EV8 事件并清除标志*/ 50 while (!I2C_CheckEvent(EEPROM_I2C,I2C_EVENT_MASTER_BYTE_TRANSMITTED)) 51 { 52 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12); 53 } 54 /* 产生第二次 I2C 起始信号 */ 55 I2C_GenerateSTART(EEPROM_I2C, ENABLE); 56 57 I2CTimeout = I2CT_FLAG_TIMEOUT; 58 59 /* 检测 EV5 事件并清除标志*/ 60 while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 61 { 62 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13); 63 } 64 /* 发送 EEPROM 设备地址 */ 65 I2C_Send7bitAddress(EEPROM_I2C, EEPROM_ADDRESS, I2C_Direction_Receiver); 66 67 I2CTimeout = I2CT_FLAG_TIMEOUT; 68 69 /* 检测 EV6 事件并清除标志*/ 70 while (!I2C_CheckEvent(EEPROM_I2C, 71 I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) 72 { 73 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14); 74 } 75 /* 读取 NumByteToRead 个数据*/ 76 while (NumByteToRead) 77 { 78 /*若 NumByteToRead=1,表示已经接收到最后一个数据了, 79 发送非应答信号,结束传输*/ 80 if (NumByteToRead == 1) 81 { 82 /* 发送非应答信号 */ 83 I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE); 84 85 /* 发送停止信号 */ 86 I2C_GenerateSTOP(EEPROM_I2C, ENABLE); 87 } 88 89 I2CTimeout = I2CT_LONG_TIMEOUT; 第 239 页 共 928 零死角玩转 STM32—F429 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 } while (I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)==0) { if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3); } { /*通过 I2C,从设备中读取一个字节的数据 */ *pBuffer = I2C_ReceiveData(EEPROM_I2C); /* 存储数据的指针指向下一个地址 */ pBuffer++; /* 接收数据自减 */ NumByteToRead--; } } /* 使能应答,方便下一次 I2C 传输 */ I2C_AcknowledgeConfig(EEPROM_I2C, ENABLE); return 1; 这段中的写过程跟前面的写字节函数类似,而读过程中接收数据时,需要使用库函数 I2C_ReceiveData 来读取。响应信号则通过库函数 I2C_AcknowledgeConfig 来发送, DISABLE 时为非响应信号,ENABLE 为响应信号。 3. main 文件 EEPROM 读写测试函数 完成基本的读写函数后,接下来我们编写一个读写测试函数来检验驱动程序,见代码 清单 23-11。 代码清单 23-11 EEPROM 读写测试函数 1 /** 2 * @brief I2C(AT24C02)读写测试 3 * @param 无 4 * @retval 正常返回 1 ,不正常返回 0 5 */ 6 uint8_t I2C_Test(void) 7{ 8 u16 i; 9 EEPROM_INFO("写入的数据"); 10 11 for ( i=0; i<=255; i++ ) //填充缓冲 12 { 13 I2c_Buf_Write[i] = i; 14 15 printf("0x%02X ", I2c_Buf_Write[i]); 16 if (i%16 == 15) 17 printf("\n\r"); 18 } 19 20 //将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中 21 //页写入方式 22 // I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256); 23 //字节写入方式 24 I2C_EE_ByetsWrite( I2c_Buf_Write, EEP_Firstpage, 256); 25 26 EEPROM_INFO("写结束"); 27 28 EEPROM_INFO("读出的数据"); 第 240 页 共 928 零死角玩转 STM32—F429 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 } //将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中 I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256); //将 I2c_Buf_Read 中的数据通过串口打印 for (i=0; i<256; i++) { if (I2c_Buf_Read[i] != I2c_Buf_Write[i]) { printf("0x%02X ", I2c_Buf_Read[i]); EEPROM_ERROR("错误:I2C EEPROM 写入与读出的数据不一致"); return 0; } printf("0x%02X ", I2c_Buf_Read[i]); if (i%16 == 15) printf("\n\r"); } EEPROM_INFO("I2C(AT24C02)读写测试成功"); return 1; 代码中先填充一个数组,数组的内容为 1,2,3 至 N,接着把这个数组的内容写入到 EEPROM 中,写入时可以采用单字节写入的方式或页写入的方式。写入完毕后再从 EEPROM 的地址中读取数据,把读取得到的与写入的数据进行校验,若一致说明读写正常, 否则读写过程有问题或者 EEPROM 芯片不正常。其中代码用到的 EEPROM_INFO 跟 EEPROM_ERROR 宏类似,都是对 printf 函数的封装,使用和阅读代码时把它直接当成 printf 函数就好。具体的宏定义在“bsp_i2c_ee.h 文件中”,在以后的代码我们常常会用类 似的宏来输出调试信息。 main 函数 最后编写 main 函数,函数中初始化了 LED、串口、I2C 外设,然后调用上面的 I2C_Test 函数进行读写测试,见代码清单 23-12。 代码清单 23-12 main 函数 1 2 /** 3 * @brief 主函数 4 * @param 无 5 * @retval 无 6 */ 7 int main(void) 8{ 9 LED_GPIO_Config(); 10 11 LED_BLUE; 12 /*初始化 USART1*/ 13 Debug_USART_Config(); 14 15 printf("\r\n 欢迎使用秉火 STM32 F429 开发板。\r\n"); 16 17 printf("\r\n 这是一个 I2C 外设(AT24C02)读写测试例程 \r\n"); 18 19 /* I2C 外设(AT24C02)初始化 */ 20 I2C_EE_Init(); 21 22 if (I2C_Test() ==1) 23 { 24 LED_GREEN; 25 } 第 241 页 共 928 零死角玩转 STM32—F429 26 27 28 29 30 31 32 33 34 35 } 36 else { LED_RED; } while (1) { } 23.4.3 下载验证 用 USB 线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。在串口调试助手可看到 EEPROM 测试的调试信息。 23.5 每课一问 1. 在 EEPROM 测试程序中,分别使用单字节写入及页写入函数写入数据,对比它们消耗 的时间。 2. 尝试使用 EEPROM 存储 int 整型变量,float 型浮点变量,编写程序写入数据,并读出 校验。 3. 尝试把 I2C 通讯引脚的模式改成非开漏模式,测试是否还能正常通讯,为什么? 4. 查看“bsp_i2c_ee.h”文件中 EEPROM_ERROR、EEPROM_INFO、EEPROM_DEBUG 宏,解释为何要使用这样的宏输出调试信息,而不直接使用 printf 函数。 第 242 页 共 928 零死角玩转 STM32—F429 第24章 SPI—读写串行 FLASH 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》及《SPI 总线协议介绍》。 若对 SPI 通讯协议不了解,可先阅读《SPI 总线协议介绍》文档的内容学习。 关于 FLASH 存储器,请参考“常用存储器介绍”章节,实验中 FLASH 芯片的具体参 数,请参考其规格书《W25Q128》来了解。 24.1 SPI 协议简介 SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设 备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间, 要求通讯速率较高的场合。 学习本章时,可与 I2C 章节对比阅读,体会两种通讯总线的差异以及 EEPROM 存储器 与 FLASH 存储器的区别。下面我们分别对 SPI 协议的物理层及协议层进行讲解。 24.1.1 SPI 物理层 SPI 通讯设备之间的常用连接方式见图 24-1。 图 24-1 常见的 SPI 通讯系统 SPI 通讯使用 3 条总线及片选线,3 条总线分别为 SCK、MOSI、MISO,片选线为 ———— S S ,它们的作用介绍如下: 第 243 页 共 928 零死角玩转 STM32—F429 ———— (1) S S ( Slave Select):从设备选择信号线,常称为片选信号线,也称为 NSS、CS,以下 用 NSS 表示。当有多个 SPI 从设备与 SPI 主机相连时,设备的其它信号线 SCK、 MOSI 及 MISO 同时并联到相同的 SPI 总线上,即无论有多少个从设备,都共同只使用 这 3 条总线;而每个从设备都有独立的这一条 NSS 信号线,本信号线独占主机的一个 引脚,即有多少个从设备,就有多少条片选信号线。I2C 协议中通过设备地址来寻址、 选中总线上的某个设备并与其进行通讯;而 SPI 协议中没有设备地址,它使用 NSS 信 号线来寻址,当主机要选择从设备时,把该从设备的 NSS 信号线设置为低电平,该从 设备即被选中,即片选有效,接着主机开始与被选中的从设备进行 SPI 通讯。所以 SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。。 (2) SCK (Serial Clock):时钟信号线,用于通讯数据同步。它由通讯主机产生,决定了通 讯的速率,不同的设备支持的最高时钟频率不一样,如 STM32 的 SPI 时钟频率最大为 fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。 (3) MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。主机的数据从这条 信号线输出,从机由这条信号线读入主机发送的数据,即这条线上数据的方向为主机 到从机。 (4) MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。主机从这条信号线 读入数据,从机的数据由这条信号线输出到主机,即在这条线上数据的方向为从机到 主机。 24.1.2 协议层 与 I2C 的类似,SPI 协议定义了通讯的起始和停止信号、数据有效性、时钟同步等环 节。 1. SPI 基本通讯过程 先看看 SPI 通讯的通讯时序,见图 24-2。 图 24-2 SPI 通讯时序 第 244 页 共 928 零死角玩转 STM32—F429 这是一个主机的通讯时序。NSS、SCK、MOSI 信号都由主机控制产生,而 MISO 的信 号由从机产生,主机通过该信号线读取从机的数据。MOSI 与 MISO 的信号只在 NSS 为低 电平的时候才有效,在 SCK 的每个时钟周期 MOSI 和 MISO 传输一位数据。 以上通讯流程中包含的各个信号分解如下: 2. 通讯的起始和停止信号 在图 24-2 中的标号处,NSS 信号线由高变低,是 SPI 通讯的起始信号。NSS 是每个 从机各自独占的信号线,当从机检在自己的 NSS 线检测到起始信号后,就知道自己被主机 选中了,开始准备与主机通讯。在图中的标号处,NSS 信号由低变高,是 SPI 通讯的停 止信号,表示本次通讯结束,从机的选中状态被取消。 3. 数据有效性 SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI 及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的。数 据传输时,MSB 先行或 LSB 先行并没有作硬性规定,但要保证两个 SPI 通讯设备之间使用 同样的协定,一般都会采用图 24-2 中的 MSB 先行模式。 观察图中的标号处,MOSI 及 MISO 的数据在 SCK 的上升沿期间变化输出, 在 SCK 的下降沿时被采样。即在 SCK 的下降沿时刻,MOSI 及 MISO 的数据有效,高电平 时表示数据“1”,为低电平时表示数据“0”。在其它时刻,数据无效,MOSI 及 MISO 为下一次表示数据做准备。 SPI 每次数据传输可以 8 位或 16 位为单位,每次传输的单位数不受限制。 4. CPOL/CPHA 及通讯模式 上面讲述的图 24-2 中的时序只是 SPI 中的其中一种通讯模式,SPI 一共有四种通讯模 式,它们的主要区别是总线空闲时 SCK 的时钟状态以及数据采样时刻。为方便说明,在此 引入“时钟极性 CPOL”和“时钟相位 CPHA”的概念。 时钟极性 CPOL 是指 SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通 讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平, CPOL=1 时,则相反。 时钟相位 CPHA 是指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的 信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数 边沿”采样。见图 24-3 及图 24-4。 第 245 页 共 928 零死角玩转 STM32—F429 图 24-3 CPHA=0 时的 SPI 通讯模式 我们来分析这个 CPHA=0 的时序图。首先,根据 SCK 在空闲状态时的电平,分为两 种情况。SCK 信号线在空闲状态为低电平时,CPOL=0;空闲状态为高电平时,CPOL=1。 无论 CPOL=0 还是=1,因为我们配置的时钟相位 CPHA=0,在图中可以看到,采样时 刻都是在 SCK 的奇数边沿。注意当 CPOL=0 的时候,时钟的奇数边沿是上升沿,而 CPOL=1 的时候,时钟的奇数边沿是下降沿。所以 SPI 的采样时刻不是由上升/下降沿决定 的。MOSI 和 MISO 数据线的有效信号在 SCK 的奇数边沿保持不变,数据信号将在 SCK 奇 数边沿时被采样,在非采样时刻,MOSI 和 MISO 的有效信号才发生切换。 类似地,当 CPHA=1 时,不受 CPOL 的影响,数据信号在 SCK 的偶数边沿被采样, 见图 24-4。 图 24-4 CPHA=1 时的 SPI 通讯模式 第 246 页 共 928 零死角玩转 STM32—F429 由 CPOL 及 CPHA 的不同状态,SPI 分成了四种模式,见表 24-1,主机与从机需要工 作在相同的模式下才可以正常通讯,实际中采用较多的是“模式 0”与“模式 3”。 表 24-1 SPI 的四种模式 SPI 模式 0 1 2 3 CPOL 0 0 1 1 CPHA 0 1 0 1 空闲时 SCK 时钟 低电平 低电平 高电平 高电平 采样时刻 奇数 边沿 偶数 边沿 奇数 边沿 偶数 边沿 24.2 STM32 的 SPI 特性及架构 与 I2C 外设一样,STM32 芯片也集成了专门用于 SPI 协议通讯的外设。 24.2.1 STM32 的 SPI 外设简介 STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2 (STM32F429 型号的芯片默认 fpclk1 为 90MHz,fpclk2 为 45MHz),完全支持 SPI 协议的 4 种 模式,数据帧长度可设置为 8 位或 16 位,可设置数据 MSB 先行或 LSB 先行。它还支持双 线全双工(前面小节说明的都是这种模式)、双线单向以及单线模式。其中双线单向模式可 以同时使用 MOSI 及 MISO 数据线向一个方向传输数据,可以加快一倍的传输速度。而单 线模式则可以减少硬件接线,当然这样速率会受到影响。我们只讲解双线全双工模式。 STM32 的 SPI 外设还支持 I2S 功能,I2S 功能是一种音频串行通讯协议,在我们以后 讲解 MP3 播放器的章节中会进行介绍。 第 247 页 共 928 零死角玩转 STM32—F429 24.2.2 STM32 的 SPI 架构剖析 图 24-5 SPI 架构图 1. 通讯引脚 SPI 的所有硬件架构都从图 24-5 中左侧 MOSI、MISO、SCK 及 NSS 线展开的。 STM32 芯片有多个 SPI 外设,它们的 SPI 通讯信号引出到不同的 GPIO 引脚上,使用时必 须配置到这些指定的引脚,见表 24-2。关于 GPIO 引脚的复用功能,可查阅《STM32F4xx 规格书》,以它为准。 表 24-2 STM32F4xx 的 SPI 引脚(整理自《STM32F4xx 规格书》) 引脚 SPI1 SPI2 SPI 编号 SPI3 SPI4 SPI5 SPI6 MOSI MISO SCK NSS PA7/PB5 PA6/PB4 PA5/PB3 PA4/PA15 PB15/PC3/PI3 PB14/PC2/PI2 PB10/PB13/PD3 PB9/PB12/PI0 PB5/PC12/PD6 PB4/PC11 PB3/PC10 PA4/PA15 PE6/PE14 PE5/PE13 PE2/PE12 PE4/PE11 PF9/PF11 PF8/PH7 PF7/PH6 PF6/PH5 PG14 PG12 PG13 PG8 其中 SPI1、SPI4、SPI5、SPI6 是 APB2 上的设备,最高通信速率达 45Mbtis/s,SPI2、 SPI3 是 APB1 上的设备,最高通信速率为 22.5Mbits/s。其它功能上没有差异。 第 248 页 共 928 零死角玩转 STM32—F429 2. 时钟控制逻辑 SCK 线的时钟信号,由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2]位控制, 该位是对 fpclk 时钟的分频因子,对 fpclk 的分频结果就是 SCK 引脚的输出时钟频率,计算方 法见表 24-3。 表 24-3 BR 位对 fpclk 的分频 BR[0:2] 分频结果(SCK 频率) BR[0:2] 分频结果(SCK 频率) 000 fpclk/2 100 fpclk/32 001 fpclk/4 101 fpclk/64 010 fpclk/8 110 fpclk/128 011 fpclk/16 111 fpclk/256 其中的 fpclk 频率是指 SPI 所在的 APB 总线频率,APB1 为 fpclk1,APB2 为 fpckl2。 通过配置“控制寄存器 CR”的“CPOL 位”及“CPHA”位可以把 SPI 设置成前面分 析的 4 种 SPI 模式。 3. 数据控制逻辑 SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的数据来源及目标 接收、发送缓冲区以及 MISO、MOSI 线。当向外发送数据的时候,数据移位寄存器以“发 送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;当从外部接收数据的时候, 数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。通过写 SPI 的“数据寄存器 DR”把数据填充到发送缓冲区中,通讯读“数据寄存器 DR”,可以获取 接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器 CR1”的“DFF 位”配置成 8 位及 16 位模式;配置“LSBFIRST 位”可选择 MSB 先行还是 LSB 先行。 4. 整体控制逻辑 整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄 存器(CR1/CR2)”的参数而改变,基本的控制参数包括前面提到的 SPI 模式、波特率、LSB 先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改 “状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位,就可以了解 SPI 的工作 状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制 NSS 信号线。 实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用 普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。 24.2.3 通讯过程 STM32 使用 SPI 外设通讯时,在通讯的不同阶段它会对“状态寄存器 SR”的不同数 据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。 图 24-6 中的是“主模式”流程,即 STM32 作为 SPI 通讯的主机端时的数据收发过程。 第 249 页 共 928 零死角玩转 STM32—F429 图 24-6 主发送器通讯过程 主模式收发流程及事件说明如下: (1) 控制 NSS 信号线,产生起始信号(图中没有画出); (2) 把要发送的数据写入到“数据寄存器 DR”中,该数据会被存储到发送缓冲区; (3) 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出 去;MISO 则把数据一位一位地存储进接收缓冲区中; (4) 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表 示传输完一帧,发送缓冲区已空;类似地,当接收完一帧数据的时候,“RXNE 标志位”会被置 1,表示传输完一帧,接收缓冲区非空; (5) 等待到“TXE 标志位”为 1 时,若还要继续发送数据,则再次往“数据寄存器 DR”写入数据即可;等待到“RXNE 标志位”为 1 时,通过读取“数据寄存器 DR”可以获取接收缓冲区中的内容。 假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进 入同一个中断服务函数,到 SPI 中断服务程序后,可通过检查寄存器位来了解是哪一个事 件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据。 24.3 SPI 初始化结构体详解 跟其它外设一样,STM32 标准库提供了 SPI 初始化结构体及初始化函数来配置 SPI 外 设。初始化结构体及函数定义在库文件“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,编程 时我们可以结合这两个文件内的注释使用或参考库帮助文档。了解初始化结构体后我们就 能对 SPI 外设运用自如了,见代码清单 24-1。 代码清单 24-1 SPI 初始化结构体 1 typedef struct 2{ 3 uint16_t SPI_Direction; 4 uint16_t SPI_Mode; 5 uint16_t SPI_DataSize; /*设置 SPI 的单双向模式 */ /*设置 SPI 的主/从机端模式 */ /*设置 SPI 的数据帧长度,可选 8/16 位 */ 第 250 页 共 928 零死角玩转 STM32—F429 6 uint16_t SPI_CPOL; 7 uint16_t SPI_CPHA; 8 uint16_t SPI_NSS; 9 uint16_t SPI_BaudRatePrescaler; 10 uint16_t SPI_FirstBit; 11 uint16_t SPI_CRCPolynomial; 12 } SPI_InitTypeDef; /*设置时钟极性 CPOL,可选高/低电平*/ /*设置时钟相位,可选奇/偶数边沿采样 */ /*设置 NSS 引脚由 SPI 硬件控制还是软件控制*/ /*设置时钟分频因子,fpclk/分频数=fSCK */ /*设置 MSB/LSB 先行 */ /*设置 CRC 校验的表达式 */ 这些结构体成员说明如下,其中括号内的文字是对应参数在 STM32 标准库中定义的宏: (1) SPI_Direction 本成员设置 SPI 的通讯方向,可设置为双线全双工(SPI_Direction_2Lines_FullDuplex), 双线只接收(SPI_Direction_2Lines_RxOnly),单线只接收(SPI_Direction_1Line_Rx)、单线只 发送模式(SPI_Direction_1Line_Tx)。 (2) SPI_Mode 本成员设置 SPI 工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),这 两个模式的最大区别为 SPI 的 SCK 信号线的时序,SCK 的时序是由通讯中的主机产生的。 若被配置为从机模式,STM32 的 SPI 外设将接受外来的 SCK 信号。 (3) SPI_DataSize 本成员可以选择 SPI 通讯的数据帧大小是为 8 位(SPI_DataSize_8b)还是 16 位 (SPI_DataSize_16b)。 (4) SPI_CPOL 和 SPI_CPHA 这两个成员配置 SPI 的时钟极性 CPOL 和时钟相位 CPHA,这两个配置影响到 SPI 的 通讯模式,关于 CPOL 和 CPHA 的说明参考前面“通讯模式”小节。 时钟极性 CPOL 成员,可设置为高电平(SPI_CPOL_High)或低电平(SPI_CPOL_Low )。 时钟相位 CPHA 则可以设置为 SPI_CPHA_1Edge(在 SCK 的奇数边沿采集数据) 或 SPI_CPHA_2Edge (在 SCK 的偶数边沿采集数据) 。 (5) SPI_NSS 本成员配置 NSS 引脚的使用模式,可以选择为硬件模式(SPI_NSS_Hard )与软件模式 (SPI_NSS_Soft ),在硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要 我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。实际中软件模式应用比 较多。 (6) SPI_BaudRatePrescaler 本成员设置波特率分频因子,分频后的时钟即为 SPI 的 SCK 信号线的时钟频率。这个 成员参数可设置为 fpclk 的 2、4、6、8、16、32、64、128、256 分频。 (7) SPI_FirstBit 第 251 页 共 928 零死角玩转 STM32—F429 所有串行的通讯协议都会有 MSB 先行(高位数据在前)还是 LSB 先行(低位数据在前)的 问题,而 STM32 的 SPI 模块可以通过这个结构体成员,对这个特性编程控制。 (8) SPI_CRCPolynomial 这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数 (多项式),来计算 CRC 的值。 配置完这些结构体成员后,我们要调用 SPI_Init 函数把这些参数写入到寄存器中,实 现 SPI 的初始化,然后调用 SPI_Cmd 来使能 SPI 外设。 24.4 SPI—读写串行 FLASH 实验 FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH 存储器容量普遍大于 EEPROM,现在基本取代了它的地位。我们生活中常用的 U 盘、SD 卡、SSD 固态硬盘以及我们 STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存 储器。在存储控制上,最主要的区别是 FLASH 芯片只能一大片一大片地擦写,而在“I2C 章节”中我们了解到 EEPROM 可以单个字节擦写。 本小节以一种使用 SPI 通讯的串行 FLASH 存储芯片的读写实验为大家讲解 STM32 的 SPI 使用方法。实验中 STM32 的 SPI 外设采用主模式,通过查询事件的方式来确保正常通 讯。 24.4.1 硬件设计 图 24-7 SPI 串行 FLASH 硬件连接图 本实验板中的 FLASH 芯片(型号:W25Q128)是一种使用 SPI 通讯协议的 NOR FLASH 存储器,它的 CS/CLK/DIO/DO 引脚分别连接到了 STM32 对应的 SDI 引脚 NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用 NSS 引脚,所以程序中我们要使用软件控制的方式。 第 252 页 共 928 零死角玩转 STM32—F429 FLASH 芯片中还有 WP 和 HOLD 引脚。WP 引脚可控制写保护功能,当该引脚为低电 平时,禁止写入数据。我们直接接电源,不使用写保护功能。HOLD 引脚可用于暂停通讯, 该引脚为低电平时,通讯暂停,数据输出引脚输出高阻抗状态,时钟和数据输入引脚无效。 我们直接接电源,不使用通讯暂停功能。 关于 FLASH 芯片的更多信息,可参考其数据手册《W25Q128》来了解。若您使用的 实验板 FLASH 的型号或控制引脚不一样,只需根据我们的工程修改即可,程序的控制原 理相同。 24.4.2 软件设计 为了使工程更加有条理,我们把读写 FLASH 相关的代码独立分开存储,方便以后移 植。在“工程模板”之上新建“bsp_spi_flash.c”及“bsp_spi_ flash.h”文件,这些文件也 可根据您的喜好命名,它们不属于 STM32 标准库的内容,是由我们自己根据应用需要编写 的。 1. 编程要点 (7) 初始化通讯使用的目标引脚及端口时钟; (8) 使能 SPI 外设的时钟; (9) 配置 SPI 外设的模式、地址、速率等参数并使能 SPI 外设; (10) 编写基本 SPI 按字节收发的函数; (11) 编写对 FLASH 擦除及读写操作的的函数; (12) 编写测试程序,对读写数据进行校验。 2. 代码分析 SPI 硬件相关宏定义 我们把 SPI 硬件相关的配置都以宏的形式定义到 “bsp_spi_ flash.h”文件中,见代码 清单 24-2。 代码清单 24-2 SPI 硬件配置相关的宏 1 //SPI 号及时钟初始化函数 2 #define FLASH_SPI 3 #define FLASH_SPI_CLK 4 #define FLASH_SPI_CLK_INIT 5 //SCK 引脚 6 #define FLASH_SPI_SCK_PIN 7 #define FLASH_SPI_SCK_GPIO_PORT 8 #define FLASH_SPI_SCK_GPIO_CLK 9 #define FLASH_SPI_SCK_PINSOURCE 10 #define FLASH_SPI_SCK_AF 11 //MISO 引脚 12 #define FLASH_SPI_MISO_PIN 13 #define FLASH_SPI_MISO_GPIO_PORT 14 #define FLASH_SPI_MISO_GPIO_CLK 15 #define FLASH_SPI_MISO_PINSOURCE 16 #define FLASH_SPI_MISO_AF 17 //MOSI 引脚 18 #define FLASH_SPI_MOSI_PIN SPI3 RCC_APB1Periph_SPI3 RCC_APB1PeriphClockCmd GPIO_Pin_3 GPIOB RCC_AHB1Periph_GPIOB GPIO_PinSource3 GPIO_AF_SPI3 GPIO_Pin_4 GPIOB RCC_AHB1Periph_GPIOB GPIO_PinSource4 GPIO_AF_SPI3 GPIO_Pin_5 第 253 页 共 928 零死角玩转 STM32—F429 19 #define FLASH_SPI_MOSI_GPIO_PORT GPIOB 20 #define FLASH_SPI_MOSI_GPIO_CLK RCC_AHB1Periph_GPIOB 21 #define FLASH_SPI_MOSI_PINSOURCE GPIO_PinSource5 22 #define FLASH_SPI_MOSI_AF GPIO_AF_SPI3 23 //CS(NSS)引脚 24 #define FLASH_CS_PIN GPIO_Pin_8 25 #define FLASH_CS_GPIO_PORT GPIOI 26 #define FLASH_CS_GPIO_CLK RCC_AHB1Periph_GPIOI 27 28 //控制 CS(NSS)引脚输出低电平 29 #define SPI_FLASH_CS_LOW() {FLASH_CS_GPIO_PORT->BSRRH=FLASH_CS_PIN;} 30 //控制 CS(NSS)引脚输出高电平 31 #define SPI_FLASH_CS_HIGH() {FLASH_CS_GPIO_PORT->BSRRL=FLASH_CS_PIN;} 以上代码根据硬件连接,把与 FLASH 通讯使用的 SPI 号 、引脚号、引脚源以及复用 功能映射都以宏封装起来,并且定义了控制 CS(NSS)引脚输出电平的宏,以便配置产生起 始和停止信号时使用。 初始化 SPI 的 GPIO 利用上面的宏,编写 SPI 的初始化函数,见代码清单 24-3。 代码清单 24-3 SPI 的初始化函数(GPIO 初始化部分) 1 2 /** 3 * @brief SPI_FLASH 初始化 4 * @param 无 5 * @retval 无 6 */ 7 void SPI_FLASH_Init(void) 8{ 9 GPIO_InitTypeDef GPIO_InitStructure; 10 11 /* 使能 FLASH_SPI 及 GPIO 时钟 */ 12 /*!< SPI_FLASH_SPI_CS_GPIO, SPI_FLASH_SPI_MOSI_GPIO, 13 SPI_FLASH_SPI_MISO_GPIO 和 SPI_FLASH_SPI_SCK_GPIO 时钟使能 */ 14 RCC_AHB1PeriphClockCmd (FLASH_SPI_SCK_GPIO_CLK | FLASH_SPI_MISO_GPIO_CLK| 15 FLASH_SPI_MOSI_GPIO_CLK|FLASH_CS_GPIO_CLK, ENABLE); 16 17 /*!< SPI_FLASH_SPI 时钟使能 */ 18 FLASH_SPI_CLK_INIT(FLASH_SPI_CLK, ENABLE); 19 20 //设置引脚复用 21 GPIO_PinAFConfig(FLASH_SPI_SCK_GPIO_PORT,FLASH_SPI_SCK_PINSOURCE, 22 FLASH_SPI_SCK_AF); 23 GPIO_PinAFConfig(FLASH_SPI_MISO_GPIO_PORT,FLASH_SPI_MISO_PINSOURCE, 24 FLASH_SPI_MISO_AF); 25 GPIO_PinAFConfig(FLASH_SPI_MOSI_GPIO_PORT,FLASH_SPI_MOSI_PINSOURCE, 26 FLASH_SPI_MOSI_AF); 27 28 /*!< 配置 SPI_FLASH_SPI 引脚: SCK */ 29 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN; 30 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 31 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 32 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 33 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 35 36 GPIO_Init(FLASH_SPI_SCK_GPIO_PORT, &GPIO_InitStructure); 37 38 /*!< 配置 SPI_FLASH_SPI 引脚: MISO */ 39 GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN; 40 GPIO_Init(FLASH_SPI_MISO_GPIO_PORT, &GPIO_InitStructure); 41 42 /*!< 配置 SPI_FLASH_SPI 引脚: MOSI */ 第 254 页 共 928 零死角玩转 STM32—F429 43 44 45 46 47 48 49 50 51 52 53 54 55 } GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN; GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT, &GPIO_InitStructure); /*!< 配置 SPI_FLASH_SPI 引脚: CS */ GPIO_InitStructure.GPIO_Pin = FLASH_CS_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_Init(FLASH_CS_GPIO_PORT, &GPIO_InitStructure); /* 停止信号 FLASH: CS 引脚高电平*/ SPI_FLASH_CS_HIGH(); /*为方便讲解,以下省略 SPI 模式初始化部分*/ //...... 与所有使用到 GPIO 的外设一样,都要先把使用到的 GPIO 引脚模式初始化,配置好 复用功能。GPIO 初始化流程如下: (1) 使用 GPIO_InitTypeDef 定义 GPIO 初始化结构体变量,以便下面用于存储 GPIO 配置; (2) 调用库函数 RCC_AHB1PeriphClockCmd 来使能 SPI 引脚使用的 GPIO 端口时钟,调用 时使用“|”操作同时配置多个引脚。调用宏 FLASH_SPI_CLK_INIT 使能 SPI 外设时 钟(该宏封装了 APB 时钟使能的库函数)。 (3) 向 GPIO 初始化结构体赋值,把 SCK/MOSI/MISO 引脚初始化成复用推挽模式。而 CS(NSS)引脚由于使用软件控制,我们把它配置为普通的推挽输出模式。 (4) 使用以上初始化结构体的配置,调用 GPIO_Init 函数向寄存器写入参数,完成 GPIO 的 初始化。 配置 SPI 的模式 以上只是配置了 SPI 使用的引脚,对 SPI 外设模式的配置。在配置 STM32 的 SPI 模式 前,我们要先了解从机端的 SPI 模式。本例子中可通过查阅 FLASH 数据手册《W25Q128》 获取。根据 FLASH 芯片的说明,它支持 SPI 模式 0 及模式 3,支持双线全双工,使用 MSB 先行模式,支持最高通讯时钟为 104MHz,数据帧长度为 8 位。我们要把 STM32 的 SPI 外设中的这些参数配置一致。见代码清单 24-4。 代码清单 24-4 配置 SPI 模式 1 /** 2 * @brief SPI_FLASH 引脚初始化 3 * @param 无 4 * @retval 无 5 */ 6 void SPI_FLASH_Init(void) 7{ 8 /*为方便讲解,省略了 SPI 的 GPIO 初始化部分*/ 9 //...... 10 11 SPI_InitTypeDef SPI_InitStructure; 12 /* FLASH_SPI 模式配置 */ 13 // FLASH 芯片 支持 SPI 模式 0 及模式 3,据此设置 CPOL CPHA 14 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; 15 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; 第 255 页 共 928 零死角玩转 STM32—F429 16 17 18 19 20 21 22 23 24 25 26 27 } SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_Init(FLASH_SPI, &SPI_InitStructure); /* 使能 FLASH_SPI */ SPI_Cmd(FLASH_SPI, ENABLE); 这段代码中,把 STM32 的 SPI 外设配置为主机端,双线全双工模式,数据帧长度为 8 位,使用 SPI 模式 3(CPOL=1,CPHA=1),NSS 引脚由软件控制以及 MSB 先行模式。最后 一个成员为 CRC 计算式,由于我们与 FLASH 芯片通讯不需要 CRC 校验,并没有使能 SPI 的 CRC 功能,这时 CRC 计算式的成员值是无效的。 赋值结束后调用库函数 SPI_Init 把这些配置写入寄存器,并调用 SPI_Cmd 函数使能外 设。 使用 SPI 发送和接收一个字节的数据 初始化好 SPI 外设后,就可以使用 SPI 通讯了,复杂的数据通讯都是由单个字节数据 收发组成的,我们看看它的代码实现,见代码清单 24-5。 代码清单 24-5 使用 SPI 发送和接收一个字节的数据 1 #define Dummy_Byte 0xFF 2 /** 3 * @brief 使用 SPI 发送一个字节的数据 4 * @param byte:要发送的数据 5 * @retval 返回接收到的数据 6 */ 7 u8 SPI_FLASH_SendByte(u8 byte) 8{ 9 SPITimeout = SPIT_FLAG_TIMEOUT; 10 11 /* 等待发送缓冲区为空,TXE 事件 */ 12 while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE) == RESET) 13 { 14 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0); 15 } 16 17 /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */ 18 SPI_I2S_SendData(FLASH_SPI, byte); 19 20 SPITimeout = SPIT_FLAG_TIMEOUT; 21 22 /* 等待接收缓冲区非空,RXNE 事件 */ 23 while (SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE) == RESET) 24 { 25 if ((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1); 26 } 27 28 /* 读取数据寄存器,获取接收缓冲区数据 */ 29 return SPI_I2S_ReceiveData(FLASH_SPI); 30 } 31 32 /** 33 * @brief 使用 SPI 读取一个字节的数据 第 256 页 共 928 零死角玩转 STM32—F429 34 * @param 无 35 * @retval 返回接收到的数据 36 */ 37 u8 SPI_FLASH_ReadByte(void) 38 { 39 return (SPI_FLASH_SendByte(Dummy_Byte)); 40 } SPI_FLASH_SendByte 发送单字节函数中包含了等待事件的超时处理,这部分原理跟 I2C 中的一样,在此不再赘述。 SPI_FLASH_SendByte 函数实现了前面讲的“SPI 通讯过程”: (1) 本函数中不包含 SPI 起始和停止信号,只是收发的主要过程,所以在调用本函数前后 要做好起始和停止信号的操作; (2) 对 SPITimeout 变量赋值为宏 SPIT_FLAG_TIMEOUT。这个 SPITimeout 变量在下面的 while 循环中每次循环减 1,该循环通过调用库函数 SPI_I2S_GetFlagStatus 检测事件, 若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测,当 检 测 SPIT_FLAG_TIMEOUT 次 都 还 没 等 待 到 事 件 则 认 为 通 讯 失 败 , 调 用 的 SPI_TIMEOUT_UserCallback 输出调试信息,并退出通讯; (3) 通过检测 TXE 标志,获取发送缓冲区的状态,若发送缓冲区为空,则表示可能存在的 上一个数据已经发送完毕; (4) 等待至发送缓冲区为空后,调用库函数 SPI_I2S_SendData 把要发送的数据“byte”写 入到 SPI 的数据寄存器 DR,写入 SPI 数据寄存器的数据会存储到发送缓冲区,由 SPI 外设发送出去; (5) 写入完毕后等待 RXNE 事件,即接收缓冲区非空事件。由于 SPI 双线全双工模式下 MOSI 与 MISO 数据传输是同步的(请对比“SPI 通讯过程”阅读),当接收缓冲区非空 时,表示上面的数据发送完毕,且接收缓冲区也收到新的数据; (6) 等待至接收缓冲区非空时,通过调用库函数 SPI_I2S_ReceiveData 读取 SPI 的数据寄存 器 DR,就可以获取接收缓冲区中的新数据了。代码中使用关键字“return”把接收到 的这个数据作为 SPI_FLASH_SendByte 函数的返回值,所以我们可以看到在下面定义 的 SPI 接 收 数 据 函 数 SPI_FLASH_ReadByte , 它 只 是 简 单 地 调 用 了 SPI_FLASH_SendByte 函数发送数据“Dummy_Byte”,然后获取其返回值(因为不关 注发送的数据,所以此时的输入参数“Dummy_Byte”可以为任意值)。可以这样做的 原因是 SPI 的接收过程和发送过程实质是一样的,收发同步进行,关键在于我们的上 层应用中,关注的是发送还是接收的数据。 第 257 页 共 928 零死角玩转 STM32—F429 控制 FLASH 的指令 搞定 SPI 的基本收发单元后,还需要了解如何对 FLASH 芯片进行读写。FLASH 芯片 自定义了很多指令,我们通过控制 STM32 利用 SPI 总线向 FLASH 芯片发送指令,FLASH 芯片收到后就会执行相应的操作。 而这些指令,对主机端(STM32)来说,只是它遵守最基本的 SPI 通讯协议发送出的数 据,但在设备端(FLASH 芯片)把这些数据解释成不同的意义,所以才成为指令。查看 FLASH 芯片的数据手册《W25Q128》,可了解各种它定义的各种指令的功能及指令格式, 见表 24-4。 表 24-4 FLASH 常用芯片指令表(摘自规格书《W25Q128》) 指令 第一字 节(指令 编码) 第二 字节 第三字 第四字 第五 节 节 字节 第六字节 第七-N 字节 Write Enable 06h Write Disable 04h Read Status 05h Register Write Status 01h Register Read Data 03h Fast Read 0Bh (S7–S0) (S7–S0) A23– A16 A23– A16 A15–A8 A7–A0 A15–A8 A7–A0 (D7– D0) dummy (Next byte) (D7–D0) continuous (Next Byte) continuous Fast Read Dual Output 3Bh A23– A16 A15–A8 A7–A0 dummy I/O = (D6,D4,D2,D0) O = (D7,D5,D3,D1) (one byte per 4 clocks, continuous) Page Program 02h Block Erase(64KB) D8h Sector Erase(4KB) 20h Chip Erase C7h Power-down B9h Release Power- ABh down / Device ID A23– A16 A23– A16 A23– A16 A15–A8 A7–A0 D7–D0 Next byte A15–A8 A7–A0 A15–A8 A7–A0 dummy dummy dummy (ID7ID0) Up to 256 bytes Manufacturer/ 90h Device ID dummy dummy 00h (M7M0) (ID7-ID0) JEDEC ID (M7- (ID15- (ID7- M0) ID8) ID0) 容 9Fh 生产厂 存储器 量 商 类型 该表中的第一列为指令名,第二列为指令编码,第三至第 N 列的具体内容根据指令的 不同而有不同的含义。其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应, 第 258 页 共 928 零死角玩转 STM32—F429 不带括号的则为主机向 FLASH 传输。表中“A0~A23”指 FLASH 芯片内部存储器组织的 地址;“M0~M7”为厂商号(MANUFACTURER ID);“ID0-ID15”为 FLASH 芯片的 ID;“dummy”指该处可为任意数据;“D0~D7”为 FLASH 内部存储矩阵的内容。 在 FLSAH 芯片内部,存储有固定的厂商编号(M7-M0)和不同类型 FLASH 芯片独有的 编号(ID15-ID0),见表 24-5。 表 24-5 FLASH 数据手册的设备 ID 说明 FLASH 型号 厂商号(M7-M0) FLASH 型号(ID15-ID0) W25Q64 EF h 4017 h W25Q128 EF h 4018 h 通过指令表中的读 ID 指令“JEDEC ID”可以获取这两个编号,该指令编码为“9F h”,其中“9F h”是指 16 进制数“9F” (相当于 C 语言中的 0x9F)。紧跟指令编码的三个 字节分别为 FLASH 芯片输出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。 此处我们以该指令为例,配合其指令时序图进行讲解,见图 24-8。 图 24-8 FLASH 读 ID 指令“JEDEC ID”的时序(摘自规格书《W25Q128》) 主机首先通过 MOSI 线向 FLASH 芯片发送第一个字节数据为“9F h”,当 FLASH 芯 片收到该数据后,它会解读成主机向它发送了“JEDEC 指令”,然后它就作出该命令的响 应:通过 MISO 线把它的厂商 ID(M7-M0)及芯片类型(ID15-0)发送给主机,主机接收到指 令响应后可进行校验。常见的应用是主机端通过读取设备 ID 来测试硬件是否连接正常,或 用于识别设备。 第 259 页 共 928 零死角玩转 STM32—F429 对于 FLASH 芯片的其它指令,都是类似的,只是有的指令包含多个字节,或者响应 包含更多的数据。 实际上,编写设备驱动都是有一定的规律可循的。首先我们要确定设备使用的是什么 通讯协议。如上一章的 EEPROM 使用的是 I2C,本章的 FLASH 使用的是 SPI。那么我们就 先根据它的通讯协议,选择好 STM32 的硬件模块,并进行相应的 I2C 或 SPI 模块初始化。 接着,我们要了解目标设备的相关指令,因为不同的设备,都会有相应的不同的指令。如 EEPROM 中会把第一个数据解释为内部存储矩阵的地址(实质就是指令)。而 FLASH 则定义 了更多的指令,有写指令,读指令,读 ID 指令等等。最后,我们根据这些指令的格式要求, 使用通讯协议向设备发送指令,达到控制设备的目标。 定义 FLASH 指令编码表 为了方便使用,我们把 FLASH 芯片的常用指令编码使用宏来封装起来,后面需要发 送指令编码的时候我们直接使用这些宏即可,见代码清单 24-6。 代码清单 24-6 FLASH 指令编码表 1 /*FLASH 常用命令*/ 2 #define W25X_WriteEnable 3 #define W25X_WriteDisable 4 #define W25X_ReadStatusReg 5 #define W25X_WriteStatusReg 6 #define W25X_ReadData 7 #define W25X_FastReadData 8 #define W25X_FastReadDual 9 #define W25X_PageProgram 10 #define W25X_BlockErase 11 #define W25X_SectorErase 12 #define W25X_ChipErase 13 #define W25X_PowerDown 14 #define W25X_ReleasePowerDown 15 #define W25X_DeviceID 16 #define W25X_ManufactDeviceID 17 #define W25X_JedecDeviceID 18 /*其它*/ 19 #define sFLASH_ID 20 #define Dummy_Byte 读取 FLASH 芯片 ID 0x06 0x04 0x05 0x01 0x03 0x0B 0x3B 0x02 0xD8 0x20 0xC7 0xB9 0xAB 0xAB 0x90 0x9F 0XEF4018 0xFF 根据“JEDEC”指令的时序,我们把读取 FLASH ID 的过程编写成一个函数,见代码 清单 24-7。 代码清单 24-7 读取 FLASH 芯片 ID 1 /** 2 * @brief 读取 FLASH ID 3 * @param 无 4 * @retval FLASH ID 5 */ 6 u32 SPI_FLASH_ReadID(void) 7{ 8 u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0; 9 10 /* 开始通讯:CS 低电平 */ 11 SPI_FLASH_CS_LOW(); 12 13 /* 发送 JEDEC 指令,读取 ID */ 14 SPI_FLASH_SendByte(W25X_JedecDeviceID); 第 260 页 共 928 零死角玩转 STM32—F429 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 } /* 读取一个字节数据 */ Temp0 = SPI_FLASH_SendByte(Dummy_Byte); /* 读取一个字节数据 */ Temp1 = SPI_FLASH_SendByte(Dummy_Byte); /* 读取一个字节数据 */ Temp2 = SPI_FLASH_SendByte(Dummy_Byte); /* 停止通讯:CS 高电平 */ SPI_FLASH_CS_HIGH(); /*把数据组合起来,作为函数的返回值*/ Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2; return Temp; 这段代码利用控制 CS 引脚电平的宏“SPI_FLASH_CS_LOW/HIGH”以及前面编写的 单字节收发函数 SPI_FLASH_SendByte,很清晰地实现了“JEDEC ID”指令的时序:发送 一个字节的指令编码“W25X_JedecDeviceID”,然后读取 3 个字节,获取 FLASH 芯片对 该指令的响应,最后把读取到的这 3 个数据合并到一个变量 Temp 中,然后作为函数返回 值,把该返回值与我们定义的宏“sFLASH_ID”对比,即可知道 FLASH 芯片是否正常。 FLASH 写使能以及读取当前状态 在向 FLASH 芯片存储矩阵写入数据前,首先要使能写操作,通过“Write Enable”命 令即可写使能,见代码清单 24-8。 代码清单 24-8 写使能命令 1 /** 2 * @brief 向 FLASH 发送 写使能 命令 3 * @param none 4 * @retval none 5 */ 6 void SPI_FLASH_WriteEnable(void) 7{ 8 /* 通讯开始:CS 低 */ 9 SPI_FLASH_CS_LOW(); 10 11 /* 发送写使能命令*/ 12 SPI_FLASH_SendByte(W25X_WriteEnable); 13 14 /*通讯结束:CS 高 */ 15 SPI_FLASH_CS_HIGH(); 16 } 与 EEPROM 一样,由于 FLASH 芯片向内部存储矩阵写入数据需要消耗一定的时间, 并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH 芯片“空闲” 时才能进行再次写入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器,见 图 24-9。 第 261 页 共 928 零死角玩转 STM32—F429 图 24-9 FLASH 芯片的状态寄存器 我们只关注这个状态寄存器的第 0 位“BUSY”,当这个位为“1”时,表明 FLASH 芯片处于忙碌状态,它可能正在对内部的存储矩阵进行“擦除”或“数据写入”的操作。 利用指令表中的“Read Status Register”指令可以获取 FLASH 芯片状态寄存器的内容, 其时序见图 24-10。 图 24-10 读取状态寄存器的时序 只要向 FLASH 芯片发送了读状态寄存器的指令,FLASH 芯片就会持续向主机返回最 新的状态寄存器内容,直到收到 SPI 通讯的停止信号。据此我们编写了具有等待 FLASH 芯 片写入结束功能的函数,见代码清单 24-9。 代码清单 24-9 通过读状态寄存器等待 FLASH 芯片空闲 1 /*WIP(BUSY)标志:FLASH 内部正在写入*/ 2 #define WIP_Flag 0x01 3 4 /** 5 * @brief 等待 WIP(BUSY)标志被置 0,即等待到 FLASH 内部数据写入完毕 6 * @param none 7 * @retval none 8 */ 9 void SPI_FLASH_WaitForWriteEnd(void) 10 { 11 u8 FLASH_Status = 0; 12 /* 选择 FLASH: CS 低 */ 第 262 页 共 928 零死角玩转 STM32—F429 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 } SPI_FLASH_CS_LOW(); /* 发送 读状态寄存器 命令 */ SPI_FLASH_SendByte(W25X_ReadStatusReg); SPITimeout = SPIT_FLAG_TIMEOUT; /* 若 FLASH 忙碌,则等待 */ do { /* 读取 FLASH 芯片的状态寄存器 */ FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte); if ((SPITimeout--) == 0) { SPI_TIMEOUT_UserCallback(4); return; } } while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */ /* 停止信号 FLASH: CS 高 */ SPI_FLASH_CS_HIGH(); 这段代码发送读状态寄存器的指令编码“W25X_ReadStatusReg”后,在 while 循环里 持续获取寄存器的内容并检验它的“WIP_Flag 标志”(即 BUSY 位),一直等待到该标志表 示写入结束时才退出本函数,以便继续后面与 FLASH 芯片的数据通讯。 FLASH 扇区擦除 由于 FLASH 存储器的特性决定了它只能把原来为“1”的数据位改写成“0”,而原 来为“0”的数据位不能直接改写为“1”。所以这里涉及到数据“擦除”的概念,在写入 前,必须要对目标存储矩阵进行擦除操作,把矩阵中的数据位擦除为“1”,在数据写入的 时候,如果要存储数据“1”,那就不修改存储矩阵 ,在要存储数据“0”时,才更改该位。 通常,对存储矩阵擦除的基本操作单位都是多个字节进行,如本例子中的 FLASH 芯 片支持“扇区擦除”、“块擦除”以及“整片擦除”,见表 24-6。 表 24-6 本实验 FLASH 芯片的擦除单位 擦除单位 扇区擦除 Sector Erase 大小 4KB 块擦除 Block Erase 64KB 整片擦除 Chip Erase 整个芯片完全擦除 FLASH 芯片的最小擦除单位为扇区(Sector),而一个块(Block)包含 16 个扇区,其内部 存储矩阵分布见图 24-11。。 第 263 页 共 928 零死角玩转 STM32—F429 图 24-11 FLASH 芯片的存储矩阵 使用扇区擦除指令“Sector Erase”可控制 FLASH 芯片开始擦写,其指令时序见图 24-14。 图 24-12 扇区擦除时序 扇区擦除指令的第一个字节为指令编码,紧接着发送的 3 个字节用于表示要擦除的存 储矩阵地址。要注意的是在扇区擦除指令前,还需要先发送“写使能”指令,发送扇区擦 除指令后,通过读取寄存器状态等待扇区擦除操作完毕,代码实现见代码清单 24-10。 第 264 页 共 928 零死角玩转 STM32—F429 代码清单 24-10 擦除扇区 1 /** 2 * @brief 擦除 FLASH 扇区 3 * @param SectorAddr:要擦除的扇区地址 4 * @retval 无 5 */ 6 void SPI_FLASH_SectorErase(u32 SectorAddr) 7{ 8 /* 发送 FLASH 写使能命令 */ 9 SPI_FLASH_WriteEnable(); 10 SPI_FLASH_WaitForWriteEnd(); 11 /* 擦除扇区 */ 12 /* 选择 FLASH: CS 低电平 */ 13 SPI_FLASH_CS_LOW(); 14 /* 发送扇区擦除指令*/ 15 SPI_FLASH_SendByte(W25X_SectorErase); 16 /*发送擦除扇区地址的高位*/ 17 SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); 18 /* 发送擦除扇区地址的中位 */ 19 SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); 20 /* 发送擦除扇区地址的低位 */ 21 SPI_FLASH_SendByte(SectorAddr & 0xFF); 22 /* 停止信号 FLASH: CS 高电平 */ 23 SPI_FLASH_CS_HIGH(); 24 /* 等待擦除完毕*/ 25 SPI_FLASH_WaitForWriteEnd(); 26 } 这段代码调用的函数在前面都已讲解,只要注意发送擦除地址时高位在前即可。调用 扇区擦除指令时注意输入的地址要对齐到 4KB。 FLASH 的页写入 目标扇区被擦除完毕后,就可以向它写入数据了。与 EEPROM 类似,FLASH 芯片也 有页写入命令,使用页写入命令最多可以一次向 FLASH 传输 256 个字节的数据,我们把 这个单位为页大小。FLASH 页写入的时序见图 24-13。 图 24-13 FLASH 芯片页写入 第 265 页 共 928 零死角玩转 STM32—F429 从时序图可知,第 1 个字节为“页写入指令”编码,2-4 字节为要写入的“地址 A”, 接着的是要写入的内容,最多个可以发送 256 字节数据,这些数据将会从“地址 A”开始, 按顺序写入到 FLASH 的存储矩阵。若发送的数据超出 256 个,则会覆盖前面发送的数据。 与擦除指令不一样,页写入指令的地址并不要求按 256 字节对齐,只要确认目标存储 单元是擦除状态即可(即被擦除后没有被写入过)。所以,若对“地址 x”执行页写入指令后, 发送了 200 个字节数据后终止通讯,下一次再执行页写入指令,从“地址(x+200)”开始写 入 200 个字节也是没有问题的(小于 256 均可)。 只是在实际应用中由于基本擦除单元是 4KB,一般都以扇区为单位进行读写,想深入了解,可学习我们的“FLASH 文件系统”相 关的例子。 把页写入时序封装成函数,其实现见代码清单 24-11。 代码清单 24-11 FLASH 的页写入 1 /** 2 * @brief 对 FLASH 按页写入数据,调用本函数写入数据前需要先擦除扇区 3 * @param pBuffer,要写入数据的指针 4 * @param WriteAddr,写入地址 5 * @param NumByteToWrite,写入数据长度,必须小于等于页大小 6 * @retval 无 7 */ 8 void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite) 9{ 10 /* 发送 FLASH 写使能命令 */ 11 SPI_FLASH_WriteEnable(); 12 13 /* 选择 FLASH: CS 低电平 */ 14 SPI_FLASH_CS_LOW(); 15 /* 写送写指令*/ 16 SPI_FLASH_SendByte(W25X_PageProgram); 17 /*发送写地址的高位*/ 18 SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16); 19 /*发送写地址的中位*/ 20 SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8); 21 /*发送写地址的低位*/ 22 SPI_FLASH_SendByte(WriteAddr & 0xFF); 23 24 if (NumByteToWrite > SPI_FLASH_PerWritePageSize) 25 { 26 NumByteToWrite = SPI_FLASH_PerWritePageSize; 27 FLASH_ERROR("SPI_FLASH_PageWrite too large!"); 28 } 29 30 /* 写入数据*/ 31 while (NumByteToWrite--) 32 { 33 /* 发送当前要写入的字节数据 */ 34 SPI_FLASH_SendByte(*pBuffer); 35 /* 指向下一字节数据 */ 36 pBuffer++; 37 } 38 39 /* 停止信号 FLASH: CS 高电平 */ 40 SPI_FLASH_CS_HIGH(); 41 42 /* 等待写入完毕*/ 43 SPI_FLASH_WaitForWriteEnd(); 44 } 第 266 页 共 928 零死角玩转 STM32—F429 这段代码的内容为:先发送“写使能”命令,接着才开始页写入时序,然后发送指令 编码、地址,再把要写入的数据一个接一个地发送出去,发送完后结束通讯,检查 FLASH 状态寄存器,等待 FLASH 内部写入结束。 不定量数据写入 应用的时候我们常常要写入不定量的数据,直接调用“页写入”函数并不是特别方便, 所以我们在它的基础上编写了“不定量数据写入”的函数,基实现见代码清单 24-12。 代码清单 24-12 不定量数据写入 1 /** 2 * @brief 对 FLASH 写入数据,调用本函数写入数据前需要先擦除扇区 3 * @param pBuffer,要写入数据的指针 4 * @param WriteAddr,写入地址 5 * @param NumByteToWrite,写入数据长度 6 * @retval 无 7 */ 8 void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite) 9{ 10 u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0; 11 12 /*mod 运算求余,若 writeAddr 是 SPI_FLASH_PageSize 整数倍,运算结果 Addr 值为 0*/ 13 Addr = WriteAddr % SPI_FLASH_PageSize; 14 15 /*差 count 个数据值,刚好可以对齐到页地址*/ 16 count = SPI_FLASH_PageSize - Addr; 17 /*计算出要写多少整数页*/ 18 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; 19 /*mod 运算求余,计算出剩余不满一页的字节数*/ 20 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; 21 22 /* Addr=0,则 WriteAddr 刚好按页对齐 aligned */ 23 if (Addr == 0) 24 { 25 /* NumByteToWrite < SPI_FLASH_PageSize */ 26 if (NumOfPage == 0) 27 { 28 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite); 29 } 30 else /* NumByteToWrite > SPI_FLASH_PageSize */ 31 { 32 /*先把整数页都写了*/ 33 while (NumOfPage--) 34 { 35 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize); 36 WriteAddr += SPI_FLASH_PageSize; 37 pBuffer += SPI_FLASH_PageSize; 38 } 39 40 /*若有多余的不满一页的数据,把它写完*/ 41 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); 42 } 43 } 44 /* 若地址与 SPI_FLASH_PageSize 不对齐 */ 45 else 46 { 47 /* NumByteToWrite < SPI_FLASH_PageSize */ 48 if (NumOfPage == 0) 49 { 50 /*当前页剩余的 count 个位置比 NumOfSingle 小,写不完*/ 51 if (NumOfSingle > count) 第 267 页 共 928 零死角玩转 STM32—F429 52 { 53 temp = NumOfSingle - count; 54 55 /*先写满当前页*/ 56 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count); 57 WriteAddr += count; 58 pBuffer += count; 59 60 /*再写剩余的数据*/ 61 SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp); 62 } 63 else /*当前页剩余的 count 个位置能写完 NumOfSingle 个数据*/ 64 { 65 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite); 66 } 67 } 68 else /* NumByteToWrite > SPI_FLASH_PageSize */ 69 { 70 /*地址不对齐多出的 count 分开处理,不加入这个运算*/ 71 NumByteToWrite -= count; 72 NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; 73 NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; 74 75 SPI_FLASH_PageWrite(pBuffer, WriteAddr, count); 76 WriteAddr += count; 77 pBuffer += count; 78 79 /*把整数页都写了*/ 80 while (NumOfPage--) 81 { 82 SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize); 83 WriteAddr += SPI_FLASH_PageSize; 84 pBuffer += SPI_FLASH_PageSize; 85 } 86 /*若有多余的不满一页的数据,把它写完*/ 87 if (NumOfSingle != 0) 88 { 89 SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle); 90 } 91 } 92 } 93 } 这段代码与 EEPROM 章节中的“快速写入多字节”函数原理是一样的,运算过程在此 不再赘述。区别是页的大小以及实际数据写入的时候,使用的是针对 FLASH 芯片的页写 入函数,且在实际调用这个“不定量数据写入”函数时,还要注意确保目标扇区处于擦除 状态。 从 FLASH 读取数据 相对于写入,FLASH 芯片的数据读取要简单得多,使用读取指令“Read Data”即可, 其指令时序见图 24-14。 图 24-14 EEPROM 页写入时序(摘自《AT24C02》规格书) 第 268 页 共 928 零死角玩转 STM32—F429 发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回存储矩 阵的内容,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。 代码实现见代码清单 24-13。 代码清单 24-13 从 FLASH 读取数据 1 /** 2 * @brief 读取 FLASH 数据 3 * @param pBuffer,存储读出数据的指针 4 * @param ReadAddr,读取地址 5 * @param NumByteToRead,读取数据长度 6 * @retval 无 7 */ 8 void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead) 9{ 10 /* 选择 FLASH: CS 低电平 */ 11 SPI_FLASH_CS_LOW(); 12 13 /* 发送 读 指令 */ 14 SPI_FLASH_SendByte(W25X_ReadData); 15 16 /* 发送 读 地址高位 */ 17 SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16); 18 /* 发送 读 地址中位 */ 19 SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8); 20 /* 发送 读 地址低位 */ 21 SPI_FLASH_SendByte(ReadAddr & 0xFF); 22 23 /* 读取数据 */ 24 while (NumByteToRead--) 25 { 26 /* 读取一个字节*/ 27 *pBuffer = SPI_FLASH_SendByte(Dummy_Byte); 28 /* 指向下一个字节缓冲区 */ 29 pBuffer++; 30 } 31 32 /* 停止信号 FLASH: CS 高电平 */ 33 SPI_FLASH_CS_HIGH(); 34 } 由于读取的数据量没有限制,所以发送读命令后一直接收 NumByteToRead 个数据到结 束即可。 3. main 函数 最后我们来编写 main 函数,进行 FLASH 芯片读写校验,见代码清单 24-14。 代码清单 24-14 main 函数 1 /* 获取缓冲区的长度 */ 2 #define TxBufferSize1 (countof(TxBuffer1) - 1) 3 #define RxBufferSize1 (countof(TxBuffer1) - 1) 4 #define countof(a) (sizeof(a) / sizeof(*(a))) 5 #define BufferSize (countof(Tx_Buffer)-1) 6 7 #define FLASH_WriteAddress 0x00000 8 #define FLASH_ReadAddress FLASH_WriteAddress 9 #define FLASH_SectorToErase FLASH_WriteAddress 10 11 12 /* 发送缓冲区初始化 */ 第 269 页 共 928 零死角玩转 STM32—F429 13 uint8_t Tx_Buffer[] = "感谢您选用秉火 stm32 开发板\r\n"; 14 uint8_t Rx_Buffer[BufferSize]; 15 16 //读取的 ID 存储位置 17 __IO uint32_t DeviceID = 0; 18 __IO uint32_t FlashID = 0; 19 __IO TestStatus TransferStatus1 = FAILED; 20 21 // 函数原型声明 22 void Delay(__IO uint32_t nCount); 23 24 /* 25 * 函数名:main 26 * 描述 :主函数 27 * 输入 :无 28 * 输出 :无 29 */ 30 int main(void) 31 { 32 LED_GPIO_Config(); 33 LED_BLUE; 34 35 /* 配置串口 1 为:115200 8-N-1 */ 36 Debug_USART_Config(); 37 38 printf("\r\n 这是一个 16M 串行 flash(W25Q128)实验 \r\n"); 39 40 /* 16M 串行 flash W25Q128 初始化 */ 41 SPI_FLASH_Init(); 42 43 Delay( 200 ); 44 45 /* 获取 SPI Flash ID */ 46 FlashID = SPI_FLASH_ReadID(); 47 48 /* 检验 SPI Flash ID */ 49 if (FlashID == sFLASH_ID) 50 { 51 printf("\r\n 检测到 SPI FLASH W25Q128 !\r\n"); 52 53 /* 擦除将要写入的 SPI FLASH 扇区,FLASH 写入前要先擦除 */ 54 SPI_FLASH_SectorErase(FLASH_SectorToErase); 55 56 /* 将发送缓冲区的数据写到 flash 中 */ 57 SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize); 58 printf("\r\n 写入的数据为:\r\n%s", Tx_Buffer); 59 60 /* 将刚刚写入的数据读出来放到接收缓冲区中 */ 61 SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize); 62 printf("\r\n 读出的数据为:\r\n%s", Rx_Buffer); 63 64 /* 检查写入的数据与读出的数据是否相等 */ 65 TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize); 66 67 if ( PASSED == TransferStatus1 ) 68 { 69 LED_GREEN; 70 printf("\r\n16M 串行 flash(W25Q128)测试成功!\n\r"); 71 } 72 else 73 { 74 LED_RED; 75 printf("\r\n16M 串行 flash(W25Q128)测试失败!\n\r"); 76 } 77 }// if (FlashID == sFLASH_ID) 第 270 页 共 928 零死角玩转 STM32—F429 78 else 79 { 80 LED_RED; 81 printf("\r\n 获取不到 W25Q128 ID!\n\r"); 82 } 83 84 SPI_Flash_PowerDown(); 85 while (1); 86 } 函数中初始化了 LED、串口、SPI 外设,然后读取 FLASH 芯片的 ID 进行校验,若 ID 校验通过则向 FLASH 的特定地址写入测试数据,然后再从该地址读取数据,测试读写是 否正常。 注意: 由于实验板上的 FLASH 芯片默认已经存储了特定用途的数据,如擦除了这些数据会 影响到某些程序的运行。所以我们预留了 FLASH 芯片的“第 0 扇区(0-4096 地址)”专用 于本实验,如非必要,请勿擦除其它地址的内容。如已擦除,可在配套资料里找到“刷外 部 FLASH 内容”程序,根据其说明给 FLASH 重新写入出厂内容。 24.4.3 下载验证 用 USB 线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。在串口调试助手可看到 FLASH 测试的调试信息。 24.5 每课一问 1. 在 SPI 外设初始化部分,MISO 引脚可以设置为输入模式吗?为什么?实际测试现象 如何? 2. 尝试使用 FLASH 芯片存储 int 整型变量,float 型浮点变量,编写程序写入数据,并读 出校验。 3. 如果扇区未经擦除就写入,会有什么后果?请做实验验证。 4. 简述 FLASH 存储器与 EEPROM 存储器的区别。 第 271 页 共 928 零死角玩转 STM32—F429 第25章 串行 FLASH 文件系统 FatFs 25.1 文件系统 即使读者可能不了解文件系统,读者也一定对“文件”这个概念十分熟悉。数据在 PC 上是以文件的形式储存在磁盘中的,这些数据的形式一般为 ASCII 码或二进制形式。在上 一章我们已经写好了 SPI Flash 芯片的驱动函数,我们可以非常方便的在 SPI Flash 芯片上 读写数据。如需要记录本书的书名“零死角玩转 STM32-F429 系列”,可以把这些文字转 化成 ASCII 码,存储在数组中,然后调用 SPI_FLASH_BufferWrite 函数,把数组内容写入 到 SPI Flash 芯片的指定地址上,在需要的时候从该地址把数据读取出来,再对读出来的数 据以 ASCII 码的格式进行解读。 但是,这样直接存储数据会带来极大的不便,如难以记录有效数据的位置,难以确定 存储介质的剩余空间,以及应以何种格式来解读数据。就如同一个巨大的图书馆无人管理, 杂乱无章地存放着各种书籍,难以查找所需的文档。想象一下图书馆的采购人员购书后, 把书籍往馆内一扔,拍拍屁股走人,当有人来借阅某本书的时候,就不得不一本本地查找。 这样直接存储数据的方式对于小容量的存储介质如 EEPROM 还可以接受,但对于 SPI Flash 芯片或者 SD 卡之类的大容量设备,我们需要一种高效的方式来管理它的存储内容。 这些管理方式即为文件系统,它是为了存储和管理数据,而在存储介质建立的一种组 织结构,这些结构包括操作系统引导区、目录和文件。常见的 windows 下的文件系统格式 包括 FAT32、NTFS、exFAT。在使用文件系统前,要先对存储介质进行格式化。格式化先 擦除原来内容,在存储介质上新建一个文件分配表和目录。这样,文件系统就可以记录数 据存放的物理地址,剩余空间。 使用文件系统时, 数据都以文件的形式存储。写入新文件时,先在目录中创建一个文 件索引,它指示了文件存放的物理地址,再把数据存储到该地址中。当需要读取数据时, 可以从目录中找到该文件的索引,进而在相应的地址中读取出数据。具体还涉及到逻辑地 址、簇大小、不连续存储等一系列辅助结构或处理过程。 文件系统的存在使我们在存取数据时,不再是简单地向某物理地址直接读写,而是要 遵循它的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储到不连续的 物理地址,使用目录或链表的方式来获知下一段的位置。 上一章的 SPI Flash 芯片驱动只完成了向物理地址写入数据的工作,而根据文件系统格 式的逻辑转换部分则需要额外的代码来完成。实质上,这个逻辑转换部分可以理解为当我 们需要写入一段数据时,由它来求解向什么物理地址写入数据、以什么格式写入及写入一 些原始数据以外的信息(如目录)。这个逻辑转换部分代码我们也习惯称之为文件系统。 第 272 页 共 928 零死角玩转 STM32—F429 25.2 FatFs 文件系统简介 上面提到的逻辑转换部分代码(文件系统)即为本章的要点,文件系统庞大而复杂,它 需要根据应用的文件系统格式而编写,而且一般与驱动层分离开来,很方便移植,所以工 程应用中一般是移植现成的文件系统源码。 FatFs 是面向小型嵌入式系统的一种通用的 FAT 文件系统。它完全是由 AISI C 语言编 写并且完全独立于底层的 I/O 介质。因此它可以很容易地不加修改地移植到其他的处理器 当中,如 8051、PIC、AVR、SH、Z80、H8、ARM 等。FatFs 支持 FAT12、FAT16、 FAT32 等格式,所以我们利用前面写好的 SPI Flash 芯片驱动,把 FatFs 文件系统代码移植 到工程之中,就可以利用文件系统的各种函数,对 SPI Flash 芯片以“文件”格式进行读写 操作了。 FatFs 文件系统的源码可以从 fatfs 官网下载: http://elm-chan.org/fsw/ff/00index_e.html 25.2.1 FatFs 的目录结构 在移植 FatFs 文件系统到开发板之前,我们先要到 FatFs 的官网获取源码,最新版本为 R0.11a,官网有对 FatFs 做详细的介绍,有兴趣可以了解。解压之后可看到里面有 doc 和 src 这两个文件夹,见图 25-1。doc 文件夹里面是一些使用帮助文档; src 才是 FatFs 文件系 统的源码。 图 25-1 FatFs 文件目录 25.2.2 FatFs 帮助文档 打开 doc 文件夹,可看到如图 25-2 的文件目录: 第 273 页 共 928 零死角玩转 STM32—F429 图 25-2 doc 文件夹的文件目录 其中 en 和 ja 这两个文件夹里面是编译好的 html 文档,讲的是 FATFS 里面各个函数的 使用方法,这些函数都是封装得非常好的函数,利用这些函数我们就可以操作 SPI Flash 芯 片。有关具体的函数我们在用到的时候再讲解。这两个文件夹的唯一区别就是 en 文件夹下 的文档是英文的,ja 文件夹下的是日文的。img 文件夹包含 en 和 ja 文件夹下文件需要用到 的图片,还有四个名为 app.c 文件,内容都是 FatFs 具体应用例程。00index_e.html 和 00index_j.html 是一些关于 FATFS 的简介,至于另外两个文件可以不看。 25.2.3 FATFS 源码 打开 src 文件夹,可看到如图 25-3 的文件目录: 第 274 页 共 928 零死角玩转 STM32—F429 图 25-3 src 文件夹的文件目录 option 文件夹下是一些可选的外部 c 文件,包含了多语言支持需要用到的文件和转换 函数。 diskio.c 文件是 FatFs 移植最关键的文件,它为文件系统提供了最底层的访问 SPI Flash 芯片的方法,FatFs 有且仅有它需要用到与 SPI Flash 芯片相关的函数。diskio.h 定义了 FatFs 用到的宏,以及 diskio.c 文件内与底层硬件接口相关的函数声明。 00history.txt 介绍了 FatFs 的版本更新情况。 00readme.txt 说明了当前目录下 diskio.c 、diskio.h、ff.c、ff.h、integer.h 的功能。 src 文件夹下的源码文件功能简介如下:  integer.h:文件中包含了一些数值类型定义。  diskio.c:包含底层存储介质的操作函数,这些函数需要用户自己实现,主要添加 底层驱动函数。  ff.c: FatFs 核心文件,文件管理的实现方法。该文件独立于底层介质操作文件的 函数,利用这些函数实现文件的读写。 第 275 页 共 928 零死角玩转 STM32—F429  cc936.c:本文件在 option 目录下,是简体中文支持所需要添加的文件,包含了简 体中文的 GBK 和 Unicode 相互转换功能函数。  ffconf.h:这个头文件包含了对 FatFs 功能配置的宏定义,通过修改这些宏定义就可 以裁剪 FatFs 的功能。如需要支持简体中文,需要把 ffconf.h 中的_CODE_PAGE 的宏改成 936 并把上面的 cc936.c 文件加入到工程之中。 建议阅读这些源码的顺序为:integer.h --> diskio.c --> ff.c 。 阅读文件系统源码 ff.c 文件需要一定的功底,建议读者先阅读 FAT32 的文件格式,再 去分析 ff.c 文件。若仅为使用文件系统,则只需要理解 integer.h 及 diskio.c 文件并会调用 ff.c 文件中的函数就可以了。本章主要讲解如何把 FATFS 文件系统移植到开发板上,并编 写一个简单读写操作范例。 25.3 FatFs 文件系统移植实验 25.3.1 FatFs 程序结构图 移植 FatFs 之前我们先通过 FatFs 的程序结构图了解 FatFs 在程序中的关系网络,见图 25-4。 图 25-4 FatFs 程序结构图 用户应用程序需要由用户编写,想实现什么功能就编写什么的程序,一般我们只用到 f_mount()、f_open()、f_write()、f_read()就可以实现文件的读写操作。 FatFs 组件是 FatFs 的主体,文件都在源码 src 文件夹中,其中 ff.c、ff.h、integer.h 以及 diskio.h 四个文件我们不需要改动,只需要修改 ffconf.h 和 diskio.c 两个文件。 底层设备输入输出要求实现存储设备的读写操作函数、存储设备信息获取函数等等。 我们使用 SPI Flash 芯片作为物理设备,在上一章节已经编写好了 SPI Flash 芯片的驱动程 序,这里我们就直接使用。 第 276 页 共 928 零死角玩转 STM32—F429 25.3.2 硬件设计 FatFs 属于软件组件,不需要附带其他硬件电路。我们使用 SPI Flash 芯片作为物理存 储设备,其硬件电路在上一章已经做了分析,这里就直接使用。 25.3.3 FatFs 移植步骤 上一章我们已经实现了 SPI Flash 芯片驱动程序,并实现了读写测试,为移植 FatFs 方 便,我们直接拷贝一份工程,我们在工程基础上添加 FatFs 组件,并修改 main 函数的用户 程序即可。 1) 先拷贝一份 SPI Flash 芯片测试的工程文件(整个文件夹),并修改文件夹名为“SPI —FatFs 文件系统”。将 FatFs 源码中的 src 文件夹整个文件夹拷贝一份至“SPI— FatFs 文件系统\USER\”文件夹下并修改名为“FATFS”,见图 25-5。 图 25-5 拷贝 FatFs 源码到工程 2) 使用 KEIL 软件打开工程文件(..\SPI—FatFs 文件系统\Project\RVMDK(uv5)\ BH- F429.uvprojx),并将 FatFs 组件文件添加到工程中,需要添加有 ff.c、diskio.c 和 cc936.c 三个文件,见图 25-6。 第 277 页 共 928 零死角玩转 STM32—F429 图 25-6 添加 FatFS 文件到工程 3) 添 加 FATFS 文 件 夹 到 工 程 的 include 选 项 中。 打 开 工 程 选 项 对 话 框, 选 择 “C/C++”选项下的“Include Paths”项目,在弹出路径设置对话框中选择添加 “FATFS”文件夹,见图 25-7。 第 278 页 共 928 零死角玩转 STM32—F429 图 25-7 添加 FATFS 路径到工程选项 4) 如果现在编译工程,可以发现有两个错误,一个是来自 diskio.c 文件,提示有一 些头文件没找,diskio.c 文件内容是与底层设备输入输出接口函数文件,不同硬件 设计驱动就不同,需要的文件也不同;另外一个错误来自 cc936.c 文件,提示该文 件不是工程所必需的,这是因为 FatFs 默认使用日语,我们想要支持简体中文需 要修改 FatFs 的配置,即修改 ffconf.h 文件。至此,将 FatFs 添加到工程的框架已 经操作完成,接下来要做的就是修改 diskio.c 文件和 ffconf.h 文件。 25.3.4 FatFs 底层设备驱动函数 FatFs 文件系统与底层介质的驱动分离开来,对底层介质的操作都要交给用户去实现, 它仅仅是提供了一个函数接口而已。表 25-1 为 FatFs 移植时用户必须支持的函数。通过表 25-1 我们可以清晰知道很多函数是在一定条件下才需要添加的,只有前三个函数是必须添 加的。我们完全可以根据实际需求选择实现用到的函数。 第 279 页 共 928 零死角玩转 STM32—F429 前三个函数是实现读文件最基本需求。接下来三个函数是实现创建文件、修改文件需 要的。为实现格式化功能,需要在 disk_ioctl 添加两个获取物理设备信息选项。我们 一般只有实现前面六个函数就可以了,已经足够满足大部分功能。 为支持简体中文长文件名称需要添加 ff_convert 和 ff_wtoupper 函数,实际这两个 已经在 cc936.c 文件中实现了,我们只要直接把 cc936.c 文件添加到工程中就可以了。 后面六个函数一般都不用。如真有需要可以参考 syscall.c 文件(src\option 文件 夹内)。 表 25-1 FatFs 移植需要用户支持函数 函数 条件(ffconf.h) 备注 disk_status disk_initialize disk_read 总是需要 disk_write get_fattime disk_ioctl (CTRL_SYNC) _FS_READONLY == 0 底层设备驱动函数 disk_ioctl (GET_SECTOR_COUNT) disk_ioctl (GET_BLOCK_SIZE) _USE_MKFS == 1 disk_ioctl (GET_SECTOR_SIZE) _MAX_SS != _MIN_SS disk_ioctl (CTRL_TRIM) _USE_TRIM == 1 ff_convert ff_wtoupper _USE_LFN != 0 Unicode 支持,为支持 简体中文,添加 cc936.c 到工程即可 ff_cre_syncobj ff_del_syncobj ff_req_grant ff_rel_grant _FS_REENTRANT == 1 FatFs 可重入配置,需 要多任务系统支持(一 般不需要) ff_mem_alloc ff_mem_free _USE_LFN == 3 长文件名支持,缓冲区 设置在堆空间(一般设 置_USE_LFN = 2 ) 底层设备驱动函数是存放在 diskio.c 文件,我们的目的就是把 diskio.c 中的函数接口与 SPI Flash 芯片驱动连接起来。总共有五个函数,分别为设备状态获取(disk_status)、设备初 始化(disk_initialize)、扇区读取(disk_read)、扇区写入(disk_write)、其他控制(disk_ioctl)。 接下来,我们对每个函数结合 SPI Flash 芯片驱动做详细讲解。 宏定义 代码清单 25-1 物理编号宏定义 1 /* 为每个设备定义一个物理编号 */ 2 #define ATA 0 // 预留 SD 卡使用 3 #define SPI_FLASH 1 // 外部 SPI Flash 这两个宏定义在 FatFs 中非常重要,FatFs 是支持多物理设备的,必须为每个物理设备 定义一个不同的编号。 第 280 页 共 928 零死角玩转 STM32—F429 SD 卡是预留接口,在讲解 SDIO 接口相关章节后会用到,可以实现使用读写 SD 卡内 文件。 设备状态获取 代码清单 25-2 设备状态获取 1 DSTATUS disk_status ( 2 BYTE pdrv /* 物理编号 */ 3) 4{ 5 6 DSTATUS status = STA_NOINIT; 7 8 switch (pdrv) { 9 case ATA: /* SD CARD */ 10 break; 11 12 case SPI_FLASH: 13 /* SPI Flash 状态检测:读取 SPI Flash 设备 ID */ 14 if (sFLASH_ID == SPI_FLASH_ReadID()) { 15 /* 设备 ID 读取结果正确 */ 16 status &= ~STA_NOINIT; 17 } else { 18 /* 设备 ID 读取结果错误 */ 19 status = STA_NOINIT;; 20 } 21 break; 22 23 default: 24 status = STA_NOINIT; 25 } 26 return status; 27 } disk_status 函数只有一个参数 pdrv,表示物理编号。一般我们都是使用 switch 函数实 现对 pdrv 的分支判断。对于 SD 卡只是预留接口,留空即可。对于 SPI Flash 芯片,我们直 接调用在 SPI_FLASH_ReadID()获取设备 ID,然后判断是否正确,如果正确,函数返回正 常标准;如果错误,函数返回异常标志。SPI_FLASH_ReadID()是定义在 bsp_spi_flash.c 文 件中,上一章节已做了分析。 设备初始化 代码清单 25-3 设备初始化 1 DSTATUS disk_initialize ( 2 BYTE pdrv /* 物理编号 */ 3) 4{ 5 uint16_t i; 6 DSTATUS status = STA_NOINIT; 7 switch (pdrv) { 8 case ATA: /* SD CARD */ 9 break; 10 11 case SPI_FLASH: /* SPI Flash */ 12 /* 初始化 SPI Flash */ 13 SPI_FLASH_Init(); 14 /* 延时一小段时间 */ 15 i=500; 16 while (--i); 第 281 页 共 928 零死角玩转 STM32—F429 17 18 19 20 21 22 23 24 25 26 27 } /* 唤醒 SPI Flash */ SPI_Flash_WAKEUP(); /* 获取 SPI Flash 芯片状态 */ status=disk_status(SPI_FLASH); break; default: status = STA_NOINIT; } return status; disk_initialize 函数也是有一个参数 pdrv,用来指定设备物理编号。对于 SPI Flash 芯片 我们调用 SPI_FLASH_Init()函数实现对 SPI Flash 芯片引脚 GPIO 初始化配置以及 SPI 通信 参数配置。SPI_Flash_WAKEUP()函数唤醒 SPI Flash 芯片,当 SPI Flash 芯片处于睡眠模式 时需要唤醒芯片才可以进行读写操作。 最后调用 disk_status 函数获取 SPI Flash 芯片状态,并返回状态值。 读取扇区 代码清单 25-4 扇区读取 1 DRESULT disk_read ( 2 BYTE pdrv, /* 设备物理编号(0..) */ 3 BYTE *buff, /* 数据缓存区 */ 4 DWORD sector, /* 扇区首地址 */ 5 UINT count /* 扇区个数(1..128) */ 6) 7{ 8 DRESULT status = RES_PARERR; 9 switch (pdrv) { 10 case ATA: /* SD CARD */ 11 break; 12 13 case SPI_FLASH: 14 /* 扇区偏移 6MB,外部 Flash 文件系统空间放在 SPI Flash 后面 10MB 空间 */ 15 sector+=1536; 16 SPI_FLASH_BufferRead(buff, sector <<12, count<<12); 17 status = RES_OK; 18 break; 19 20 default: 21 status = RES_PARERR; 22 } 23 return status; 24 } disk_read 函数有四个形参。pdrv 为设备物理编号。buff 是一个 BYTE 类型指针变量, buff 指向用来存放读取到数据的存储区首地址。sector 是一个 DWORD 类型变量,指定要 读取数据的扇区首地址。count 是一个 UINT 类型变量,指定扇区数量。 BYTE 类型实际是 unsigned char 类型,DWORD 类型实际是 unsigned long 类型,UINT 类型实际是 unsigned int 类型,类型定义在 integer.h 文件中。 开发板使用的 SPI Flash 芯片型号为 W25Q128FV,每个扇区大小为 4096 个字节(4KB), 总共有 16M 字节空间,为兼容后面实验程序,我们只将后部分 10MB 空间分配给 FatFs 使 用,前部分 6MB 空间用于其他实验需要,即 FatFs 是从 6MB 空间开始,为实现这个效果 需要将所有的读写地址都偏移 1536 个扇区空间。 第 282 页 共 928 零死角玩转 STM32—F429 对于 SPI Flash 芯片,主要是使用 SPI_FLASH_BufferRead()实现在指定地址读取指定长 度的数据,它接收三个参数,第一个参数为指定数据存放地址指针。第二个参数为指定数 据读取地址,这里使用左移运算符,左移 12 位实际是乘以 4096,这与每个扇区大小是息 息相关的。第三个参数为读取数据个数,也是需要使用左移运算符。 扇区写入 代码清单 25-5 扇区输入 1 DRESULT disk_write ( 2 BYTE pdrv, /* 设备物理编号(0..) */ 3 const BYTE *buff, /* 欲写入数据的缓存区 */ 4 DWORD sector, /* 扇区首地址 */ 5 UINT count /* 扇区个数(1..128) */ 6) 7{ 8 uint32_t write_addr; 9 DRESULT status = RES_PARERR; 10 if (!count) { 11 return RES_PARERR; /* Check parameter */ 12 } 13 14 switch (pdrv) { 15 case ATA: /* SD CARD */ 16 break; 17 18 case SPI_FLASH: 19 /* 扇区偏移 6MB,外部 Flash 文件系统空间放在 SPI Flash 后面 10MB 空间 */ 20 sector+=1536; 21 write_addr = sector<<12; 22 SPI_FLASH_SectorErase(write_addr); 23 SPI_FLASH_BufferWrite((u8 *)buff,write_addr,count<<12); 24 status = RES_OK; 25 break; 26 27 default: 28 status = RES_PARERR; 29 } 30 return status; 31 } disk_write 函数有四个形参,pdrv 为设备物理编号。buff 指向待写入扇区数据的首地址。 sector,指定要读取数据的扇区首地址。count 指定扇区数量。对于 SPI Flash 芯片,在写入 数据之前需要先擦除,所以用到扇区擦除函数(SPI_FLASH_SectorErase)。然后就是在调用 数据写入函数(SPI_FLASH_BufferWrite)把数据写入到指定位置内。 其他控制 代码清单 25-6 其他控制 1 DRESULT disk_ioctl ( 2 BYTE pdrv, /* 物理编号 */ 3 BYTE cmd, /* 控制指令 */ 4 void *buff /* 写入或者读取数据地址指针 */ 5) 6{ 7 DRESULT status = RES_PARERR; 8 switch (pdrv) { 9 case ATA: /* SD CARD */ 10 break; 第 283 页 共 928 零死角玩转 STM32—F429 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 } case SPI_FLASH: switch (cmd) { /* 扇区数量:2560*4096/1024/1024=10(MB) */ case GET_SECTOR_COUNT: *(DWORD * )buff = 2560; break; /* 扇区大小 */ case GET_SECTOR_SIZE : *(WORD * )buff = 4096; break; /* 同时擦除扇区个数 */ case GET_BLOCK_SIZE : *(DWORD * )buff = 1; break; } status = RES_OK; break; default: status = RES_PARERR; } return status; disk_ioctl 函数有三个形参,pdrv 为设备物理编号,cmd 为控制指令,包括发出同步信 号、获取扇区数目、获取扇区大小、获取擦除块数量等等指令,buff 为指令对应的数据指 针。 对于 SPI Flash 芯片,为支持 FatFs 格式化功能,需要用到获取扇区数量 (GET_SECTOR_COUNT)指令和获取擦除块数量(GET_BLOCK_SIZE)。另外,SD 卡扇区大 小为 512 字节,SPI Flash 芯片一般设置扇区大小为 4096 字节,所以需要用到获取扇区大小 (GET_SECTOR_SIZE)指令。 时间戳获取 代码清单 25-7 时间戳获取 1 __weak DWORD get_fattime(void) 2{ 3 /* 返回当前时间戳 */ 4 return ((DWORD)(2015 - 1980) << 25) /* Year 2015 */ 5 | ((DWORD)1 << 21) /* Month 1 */ 6 | ((DWORD)1 << 16) /* Mday 1 */ 7 | ((DWORD)0 << 11) /* Hour 0 */ 8 | ((DWORD)0 << 5) /* Min 0 */ 9 | ((DWORD)0 >> 1); /* Sec 0 */ 10 } get_fattime 函数用于获取当前时间戳,在 ff.c 文件中被调用。FatFs 在文件创建、被修 改时会记录时间,这里我们直接使用赋值方法设定时间戳。为更好的记录时间,可以使用 控制器 RTC 功能,具体要求返回值格式为:  bit31:25 ——从 1980 至今是多少年,范围是 (0..127) ;  bit24:21 ——月份,范围为 (1..12) ;  bit20:16 ——该月份中的第几日,范围为(1..31) ; 第 284 页 共 928 零死角玩转 STM32—F429  bit15:11——时,范围为 (0..23);  bit10:5 ——分,范围为 (0..59);  bit4:0 ——秒/ 2,范围为 (0..29) 。 25.3.5 FatFs 功能配置 ffconf.h 文件是 FatFs 功能配置文件,我们可以对文件内容进行修改,使得 FatFs 更符 合我们的要求。ffconf.h 对每个配置选项都做了详细的使用情况说明。下面只列出修改的配 置,其他配置采用默认即可。 代码清单 25-8 FatFs 功能配置选项 1 #define _USE_MKFS 2 #define _CODE_PAGE 3 #define _USE_LFN 4 #define _VOLUMES 5 #define _MIN_SS 6 #define _MAX_SS 1 936 2 2 512 4096 1) _USE_MKFS:格式化功能选择,为使用 FatFs 格式化功能,需要把它设置为 1。 2) _CODE_PAGE:语言功能选择,并要求把相关语言文件添加到工程宏。为支持简 体中文文件名需要使用“936”,正如在图 25-6 的操作,我们已经把 cc936.c 文件 添加到工程中。 3) _USE_LFN:长文件名支持,默认不支持长文件名,这里配置为 2,支持长文件名, 并指定使用栈空间为缓冲区。 4) _VOLUMES:指定物理设备数量,这里设置为 2,包括预留 SD 卡和 SPI Flash 芯 片。 5) _MIN_SS 、_MAX_SS:指定扇区大小的最小值和最大值。SD 卡扇区大小一般都 为 512 字 节 , SPI Flash 芯 片 扇 区 大 小 一 般 设 置 为 4096 字 节 , 所 以 需 要 把 _MAX_SS 改为 4096。 25.3.6 FatFs 功能测试 移植操作到此,就已经把 FatFs 全部添加到我们的工程了,这时我们编译功能,顺利 编译通过,没有错误。接下来,我们就可以使用编写图 25-4 中用户应用程序了。 主要的测试包括格式化测试、文件写入测试和文件读取测试三个部分,主要程序都在 main.c 文件中实现。 变量定义 代码清单 25-9 变量定义 第 285 页 共 928 零死角玩转 STM32—F429 1 FATFS fs; /* FatFs 文件系统对象 */ 2 FIL fnew; /* 文件对象 */ 3 FRESULT res_flash; /* 文件操作结果 */ 4 UINT fnum; /* 文件成功读写数量 */ 5 BYTE buffer[1024]= {0}; /* 读缓冲区 */ 6 BYTE textFileBuffer[] = /* 写缓冲区*/ 7 "欢迎使用野火 STM32 F429 开发板 今天是个好日子,新建文件系统测试文件\r\n"; FATFS 是在 ff.h 文件定义的一个结构体类型,针对的对象是物理设备,包含了物理设 备的物理编号、扇区大小等等信息,一般我们都需要为每个物理设备定义一个 FATFS 变量。 FIL 也是在 ff.h 文件定义的一个结构体类型,针对的对象是文件系统内具体的文件, 包含了文件很多基本属性,比如文件大小、路径、当前读写地址等等。如果需要在同一时 间打开多个文件进行读写,才需要定义多个 FIL 变量,不然一般定义一个 FIL 变量即可。 FRESULT 是也在 ff.h 文件定义的一个枚举类型,作为 FatFs 函数的返回值类型,主要 管理 FatFs 运行中出现的错误。总共有 19 种错误类型,包括物理设备读写错误、找不到文 件、没有挂载工作空间等等错误。这在实际编程中非常重要,当有错误出现是我们要停止 文件读写,通过返回值我们可以快速定位到错误发生的可能地点。如果运行没有错误才返 回 FR_OK。 fnum 是个 32 位无符号整形变量,用来记录实际读取或者写入数据的数组。 buffer 和 textFileBuffer 分别对应读取和写入数据缓存区,都是 8 位无符号整形数组。 主函数 代码清单 25-10 主函数 1 int main(void) 2{ 3 /* 初始化 LED */ 4 LED_GPIO_Config(); 5 LED_BLUE; 6 7 /* 初始化调试串口,一般为串口 1 */ 8 Debug_USART_Config(); 9 printf("****** 这是一个 SPI FLASH 文件系统实验 ******\r\n"); 10 11 //在外部 SPI Flash 挂载文件系统,文件系统挂载时会对 SPI 设备初始化 12 res_flash = f_mount(&fs,"1:",1); 13 14 /*----------------------- 格式化测试 ---------------------------*/ 15 /* 如果没有文件系统就格式化创建创建文件系统 */ 16 if (res_flash == FR_NO_FILESYSTEM) { 17 printf("》FLASH 还没有文件系统,即将进行格式化...\r\n"); 18 /* 格式化 */ 19 res_flash=f_mkfs("1:",0,0); 20 21 if (res_flash == FR_OK) { 22 printf("》FLASH 已成功格式化文件系统。\r\n"); 23 /* 格式化后,先取消挂载 */ 24 res_flash = f_mount(NULL,"1:",1); 25 /* 重新挂载 */ 26 res_flash = f_mount(&fs,"1:",1); 27 } else { 28 LED_RED; 29 printf("《《格式化失败。》》\r\n"); 30 while (1); 31 } 第 286 页 共 928 零死角玩转 STM32—F429 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 } } else if (res_flash!=FR_OK) { printf("!!外部 Flash 挂载文件系统失败。(%d)\r\n",res_flash); printf("!!可能原因:SPI Flash 初始化不成功。\r\n"); while (1); } else { printf("》文件系统挂载成功,可以进行读写测试\r\n"); } /*---------------------- 文件系统测试:写测试 ----------------------*/ /* 打开文件,如果文件不存在则创建它 */ printf("\r\n****** 即将进行文件写入测试... ******\r\n"); res_flash = f_open(&fnew, "1:FatFs 读写测试文件.txt", FA_CREATE_ALWAYS | FA_WRITE ); if ( res_flash == FR_OK ) { printf("》打开/创建 FatFs 读写测试文件.txt 文件成功,向文件写入数据。\r\n"); /* 将指定存储区内容写入到文件内 */ res_flash=f_write(&fnew,WriteBuffer,sizeof(WriteBuffer),&fnum); if (res_flash==FR_OK) { printf("》文件写入成功,写入字节数据:%d\n",fnum); printf("》向文件写入的数据为:\r\n%s\r\n",WriteBuffer); } else { printf("!!文件写入失败:(%d)\n",res_flash); } /* 不再读写,关闭文件 */ f_close(&fnew); } else { LED_RED; printf("!!打开/创建文件失败。\r\n"); } /*------------------- 文件系统测试:读测试 --------------------------*/ printf("****** 即将进行文件读取测试... ******\r\n"); res_flash = f_open(&fnew, "1:FatFs 读写测试文件.txt", FA_OPEN_EXISTING | FA_READ); if (res_flash == FR_OK) { LED_GREEN; printf("》打开文件成功。\r\n"); res_flash = f_read(&fnew, ReadBuffer, sizeof(ReadBuffer), &fnum); if (res_flash==FR_OK) { printf("》文件读取成功,读到字节数据:%d\r\n",fnum); printf("》读取得的文件数据为:\r\n%s \r\n", ReadBuffer); } else { printf("!!文件读取失败:(%d)\n",res_flash); } } else { LED_RED; printf("!!打开文件失败。\r\n"); } /* 不再读写,关闭文件 */ f_close(&fnew); /* 不再使用文件系统,取消挂载文件系统 */ f_mount(NULL,"1:",1); /* 操作完成,停机 */ while (1) { } 首先,初始化 RGB 彩灯和调试串口,用来指示程序进程。 FatFs 的第一步工作就是使用 f_mount 函数挂载工作区。f_mount 函数有三个形参,第 一个参数是指向 FATFS 变量指针,如果赋值为 NULL 可以取消物理设备挂载。第二个参 第 287 页 共 928 零死角玩转 STM32—F429 数为逻辑设备编号,使用设备根路径表示,与物理设备编号挂钩,在代码清单 25-1 中我们 定义 SPI Flash 芯片物理编号为 1,所以这里使用“1:”。第三个参数可选 0 或 1,1 表示 立即挂载,0 表示不立即挂载,延迟挂载。 f_mount 函数会返回一个 FRESULT 类型值,指 示运行情况。 如果 f_mount 函数返回值为 FR_NO_FILESYSTEM,说明没有 FAT 文件系统,比如新 出厂的 SPI Flash 芯片就没有 FAT 文件系统。我们就必须对物理设备进行格式化处理。使 用 f_mkfs 函数可以实现格式化操作。f_mkfs 函数有三个形参,第一个参数为逻辑设备编号; 第二参数可选 0 或者 1,0 表示设备为一般硬盘,1 表示设备为软盘。第三个参数指定扇区 大小,如果为 0,表示通过代码清单 25-6 中 disk_ioctl 函数获取。格式化成功后需要先取消 挂载原来设备,再重新挂载设备。 在设备正常挂载后,就可以进行文件读写操作了。使用文件之前,必须使用 f_open 函 数打开文件,不再使用文件必须使用 f_close 函数关闭文件,这个跟电脑端操作文件步骤类 似。f_open 函数有三个形参,第一个参数为文件对象指针。第二参数为目标文件,包含绝 对路径的文件名称和后缀名。第三个参数为访问文件模式选择,可以是打开已经存在的文 件模式、读模式、写模式、新建模式、总是新建模式等的或运行结果。比如对于写测试, 使用 FA_CREATE_ALWAYS 和 FA_WRITE 组合模式,就是总是新建文件并进行写模式。 f_close 函数用于不再对文件进行读写操作关闭文件,f_close 函数只要一个形参,为文 件对象指针。f_close 函数运行可以确保缓冲区完全写入到文件内。 成功打开文件之后就可以使用 f_write 函数和 f_read 函数对文件进行写操作和读操作。 这两个函数用到的参数是一致的,只不过一个是数据写入,一个是数据读取。f_write 函数 第一个形参为文件对象指针,使用与 f_open 函数一致即可。第二个参数为待写入数据的首 地址,对于 f_read 函数就是用来存放读出数据的首地址。第三个参数为写入数据的字节数, 对于 f_read 函数就是欲读取数据的字节数。第四个参数为 32 位无符号整形指针,这里使用 fnum 变量地址赋值给它,在运行读写操作函数后,fnum 变量指示成功读取或者写入的字 节个数。 最后,不再使用文件系统时,使用 f_mount 函数取消挂载。 25.3.7 下载验证 保证开发板相关硬件连接正确,用 USB 线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板。程序开始运行后,RGB 彩灯 为蓝色,在串口调试助手可看到格式化测试、写文件检测和读文件检测三个过程;最后如 果所有读写操作都正常,RGB 彩灯会指示为绿色,如果在运行中 FatFs 出现错误 RGB 彩灯 指示为红色。 虽然我们通过 RGB 彩灯指示和串口调试助手信息打印方法来说明 FatFs 移植成功,并 顺利通过测试,但心底总是很踏实,所谓眼见为实,虽然我们创建了“FatFs 读写测试文 件.txt”这个文件,却完全看不到实体。这个确实是个问题,因为我们这里使用 SPI Flash 芯片作为物理设备,并不像 SD 卡那么方便直接用读卡器就可以在电脑端打开验证。另外 第 288 页 共 928 零死角玩转 STM32—F429 一个问题,就目前来说,在 SPI Flash 芯片上挂载 FatFs 好像没有实际意义,无法发挥文件 系统功能。 实际上,这里归根到底就是我们目前没办法在电脑端查看 SPI Flash 芯片内 FatFs 的内 容,没办法非常方便拷贝、删除文件。我们当然不会做无用功,STM32 控制器还有一个硬 件资源可以解决上面的问题,就是 USB!我们可以通过编程把整个开发板变成一个 U 盘, 而 U 盘存储空间就是 SPI Flash 芯片的空间。这样非常方便实现文件读写。至于 USB 内容 将在 USB 相关章节讲解。 25.4 FatFs 功能使用实验 上个实验我们实现了 FatFs 的格式化、读文件和写文件功能,这个已经满足很多部分 的运用需要。有时,我们需要更多的文件操作功能,FatFs 还是提供了不少的功能的,比如 设备存储空间信息获取、读写文件指针定位、创建目录、文件移动和重命名、文件或目录 信息获取等等功能。我们接下来这个实验内容就是展示 FatFs 众多功能,提供一个很好了 范例,以后有用到相关内容,参考使用非常方便。 25.4.1 硬件设计 本实验主要使用 FatFs 软件功能,不需要其他硬件模块,使用与 FatFs 移植实验相同硬 件配置即可。 25.4.2 软件设计 上个实验我们已经移植好了 FatFs,这个例程主要是应用,所以简单起见,直接拷贝上 个实验的工程文件,保持 FatFs 底层驱动程序,我们只改 main.c 文件内容,实现应用程序。 FatFs 多项功能测试 代码清单 25-11 FatFs 多项功能测试 1 static FRESULT miscellaneous(void) 2{ 3 DIR dir; 4 FATFS *pfs; 5 DWORD fre_clust, fre_sect, tot_sect; 6 7 printf("\n*************** 设备信息获取 ***************\r\n"); 8 /* 获取设备信息和空簇大小 */ 9 res_flash = f_getfree("1:", &fre_clust, &pfs); 10 11 /* 计算得到总的扇区个数和空扇区个数 */ 12 tot_sect = (pfs->n_fatent - 2) * pfs->csize; 13 fre_sect = fre_clust * pfs->csize; 14 15 /* 打印信息(4096 字节/扇区) */ 16 printf("》设备总空间:%10lu KB。\n》可用空间: %10lu KB。\n", 17 tot_sect *4, fre_sect *4); 18 19 printf("\n******** 文件定位和格式化写入功能测试 ********\r\n"); 20 res_flash = f_open(&fnew, "1:FatFs 读写测试文件.txt", 21 FA_OPEN_EXISTING|FA_WRITE|FA_READ ); 第 289 页 共 928 零死角玩转 STM32—F429 22 if ( res_flash == FR_OK ) { 23 /* 文件定位 */ 24 res_flash = f_lseek(&fnew,f_size(&fnew)-1); 25 if (res_flash == FR_OK) { 26 /* 格式化写入,参数格式类似 printf 函数 */ 27 f_printf(&fnew,"\n 在原来文件新添加一行内容\n"); 28 f_printf(&fnew,"》设备总空间:%10lu KB。\n》可用空间: %10lu KB。\n", 29 tot_sect *4, fre_sect *4); 30 /* 文件定位到文件起始位置 */ 31 res_flash = f_lseek(&fnew,0); 32 /* 读取文件所有内容到缓存区 */ 33 res_flash = f_read(&fnew,readbuffer,f_size(&fnew),&fnum); 34 if (res_flash == FR_OK) { 35 printf("》文件内容:\n%s\n",readbuffer); 36 } 37 } 38 f_close(&fnew); 39 40 printf("\n********** 目录创建和重命名功能测试 **********\r\n"); 41 /* 尝试打开目录 */ 42 res_flash=f_opendir(&dir,"1:TestDir"); 43 if (res_flash!=FR_OK) { 44 /* 打开目录失败,就创建目录 */ 45 res_flash=f_mkdir("1:TestDir"); 46 } else { 47 /* 如果目录已经存在,关闭它 */ 48 res_flash=f_closedir(&dir); 49 /* 删除文件 */ 50 f_unlink("1:TestDir/testdir.txt"); 51 } 52 if (res_flash==FR_OK) { 53 /* 重命名并移动文件 */ 54 res_flash=f_rename("1:FatFs 读写测试文件.txt", 55 "1:TestDir/testdir.txt"); 56 } 57 } else { 58 printf("!! 打开文件失败:%d\n",res_flash); 59 printf("!! 或许需要再次运行“FatFs 移植与读写测试”工程\n"); 60 } 61 return res_flash; 62 } 首先是设备存储信息获取,目的是获取设备总容量和剩余可用空间。f_getfree 函数是 设备空闲簇信息获取函数,有三个形参,第一个参数为逻辑设备编号;第二个参数为返回 空闲簇数量;第三个参数为返回指向文件系统对象的指针。通过计算可得到设备总的扇区 个数以及空闲扇区个数,对于 SPI Flash 芯片我们设置每个扇区为 4096 字节大小。这样很 容易就算出设备存储信息。 接下来是文件读写指针定位和格式化输入功能测试。文件定位在一些场合非常有用, 比如我们需要记录多项数据,但每项数据长度不确定,但有个最长长度,使用我们就可以 使用文件定位 lseek 函数功能把数据存放在规定好的地址空间上。当我们需要读取文件内容 时就使用文件定位函数定位到对应地址读取。 使用文件读写操作之前都必须使用 f_open 函数打开文件,开始文件是读写指针是在文 件起始位置的,马上写入数据的话会覆盖原来文件内容的。这里,我们使用 f_lseek 函数定 位到文件末尾位置,再写入内容。f_lseek 函数有两个形参,第一个参数为文件对象指针, 第二个参数为需要定位的字节数,这个字节数是相对文件起始位置的,比如设置为 0,则 将文件读写指针定位到文件起始位置了。 第 290 页 共 928 零死角玩转 STM32—F429 f_printf 函数是格式化写入函数,需要把 ffconf.h 文件中的_USE_STRFUNC 配置为 1 才 支持。f_printf 函数用法类似 C 库函数 printf 函数,只是它将数据直接写入到文件中。 最后是目录创建和文件移动和重命名功能。使用 f_opendir 函数可以打开路径(这里不 区分目录和路径概念,下同),如果路径不存在则返回错误,使用 f_closedir 函数关闭已经 打开的路径。新版的 FatFs 支持相对路径功能,使路径操作更加灵活。f_opendir 函数有两 个形参,第一个参数为指向路径对象的指针,第二个参数为路径。f_closedir 函数只需要指 向路径对象的指针一个形参。 f_mkdir 函数用于创建路径,如果指定的路径不存在就创建它,创建的路径存在形式就 是文件夹。f_mkdir 函数只要一个形参,就是指定路径。 f_rename 函数是带有移动功能的重命名函数,它有两个形参,第一个参数为源文件名 称,第二个参数为目标名称。目标名称可附带路径,如果路径与源文件路径不同见移动文 件到目标路径下。 文件信息获取 代码清单 25-12 文件信息获取 1 static FRESULT file_check(void) 2{ 3 FILINFO fno; 4 5 /* 获取文件信息 */ 6 res_flash=f_stat("1:TestDir/testdir.txt",&fno); 7 if (res_flash==FR_OK) { 8 printf("“testdir.txt”文件信息:\n"); 9 printf("》文件大小: %ld(字节)\n", fno.fsize); 10 printf("》时间戳: %u/%02u/%02u, %02u:%02u\n", 11 (fno.fdate >> 9) + 1980, fno.fdate >> 5 & 15, fno.fdate & 31, 12 fno.ftime >> 11, fno.ftime >> 5 & 63); 13 printf("》属性: %c%c%c%c%c\n\n", 14 (fno.fattrib & AM_DIR) ? 'D' : '-', // 是一个目录 15 (fno.fattrib & AM_RDO) ? 'R' : '-', // 只读文件 16 (fno.fattrib & AM_HID) ? 'H' : '-', // 隐藏文件 17 (fno.fattrib & AM_SYS) ? 'S' : '-', // 系统文件 18 (fno.fattrib & AM_ARC) ? 'A' : '-'); // 档案文件 19 } 20 return res_flash; 21 } f_stat 函数用于获取文件的属性,有两个形参,第一个参数为文件路径,第二个参数为 返回指向文件信息结构体变量的指针。文件信息结构体变量包含文件的大小、最后修改时 间和日期、文件属性、短文件名以及长文件名等信息。 路径扫描 代码清单 25-13 路径扫描 1 static FRESULT scan_files (char* path) 2{ 3 FRESULT res; //部分在递归过程被修改的变量,不用全局变量 4 FILINFO fno; 5 DIR dir; 6 int i; 7 char *fn; // 文件名 8 第 291 页 共 928 零死角玩转 STM32—F429 9 #if _USE_LFN 10 /* 长文件名支持 */ 11 /* 简体中文需要 2 个字节保存一个“字”*/ 12 static char lfn[_MAX_LFN*2 + 1]; 13 fno.lfname = lfn; 14 fno.lfsize = sizeof(lfn); 15 #endif 16 //打开目录 17 res = f_opendir(&dir, path); 18 if (res == FR_OK) { 19 i = strlen(path); 20 for (;;) { 21 //读取目录下的内容,再读会自动读下一个文件 22 res = f_readdir(&dir, &fno); 23 //为空时表示所有项目读取完毕,跳出 24 if (res != FR_OK || fno.fname[0] == 0) break; 25 #if _USE_LFN 26 27 #else 28 29 #endif 30 fn = *fno.lfname ? fno.lfname : fno.fname; fn = fno.fname; //点表示当前目录,跳过 31 if (*fn == '.') continue; 32 //目录,递归读取 33 if (fno.fattrib & AM_DIR) { 34 //合成完整目录名 35 sprintf(&path[i], "/%s", fn); 36 //递归遍历 37 res = scan_files(path); 38 path[i] = 0; 39 //打开失败,跳出循环 40 if (res != FR_OK) 41 break; 42 } else { 43 printf("%s/%s\r\n", path, fn); 44 /* 可以在这里提取特定格式的文件路径 */ 45 }//else 46 } //for 47 } 48 return res; 49 } //输出文件名 scan_files 函数用来扫描指定路径下的文件。比如我们设计一个 mp3 播放器,我们需要 提取 mp3 格式文件,诸如*.txt、*.c 文件我们统统不可要的,这时我们就必须扫描路径下所 有文件并把*.mp3 或*.MP3 格式文件提取出来。这里我们提取特定格式文件,而是把所有 文件名称都通过串口打印出来。 我们在 ffconf.h 文件中定义了长文件名称支持(_USE_LFN=2),一般有用到简体中文文 件名称的都要长文件名支持。短文件名称是 8.3 格式,即名称是 8 个字节,后缀名是 3 个 字节,对于使用英文名称还可以,使用中文名称就很容易长度不够了。使能了长文件名支 持后,使用之前需要指定文件名的存储区还有存储区的大小。 接下来就是使用 f_opendir 函数打开指定的路径。如果路径存在就使用 f_readdir 函数读 取路径下内容,f_readdir 函数可以读取路径下的文件或者文件夹,并保存信息到文件信息 对象变量内。f_readdir 函数有两个形参,第一个参数为指向路径对象变量的指针,第二个 参数为指向文件信息对象的指针。f_readdir 函数另外一个特性是自动读取下一个文件对象, 即循序运行该函数可以读取该路径下的所有文件。所以,在程序中,我们使用 for 循环让 f_readdir 函数读取所有文件,并在读取所有文件之后退出循环。 第 292 页 共 928 零死角玩转 STM32—F429 在 f_readdir 函数成功读取到一个对象时,我们还不清楚它是一个文件还是一个文件夹, 此时我们就可以使用文件信息对象变量的文件属性来判断了,如果判断得出是个文件那我 们就直接通过串口打印出来就好了。如果是个文件夹,我们就要进入该文件夹扫描,这时 就重新调用扫描函数 scan_files 就可以了,形成一个递归调用结构,只是我们这次用的参数 与最开始时候是不同的,现在是使用子文件夹名称。 主函数 代码清单 25-14 主函数 1 int main(void) 2{ 3 /* 初始化调试串口,一般为串口 1 */ 4 Debug_USART_Config(); 5 printf("******** 这是一个 SPI FLASH 文件系统实验 *******\r\n"); 6 7 //在外部 SPI Flash 挂载文件系统,文件系统挂载时会对 SPI 设备初始化 8 res_flash = f_mount(&fs,"1:",1); 9 if (res_flash!=FR_OK) { 10 printf("!!外部 Flash 挂载文件系统失败。(%d)\r\n",res_flash); 11 printf("!!可能原因:SPI Flash 初始化不成功。\r\n"); 12 while (1); 13 } else { 14 printf("》文件系统挂载成功,可以进行测试\r\n"); 15 } 16 17 /* FatFs 多项功能测试 */ 18 res_flash = miscellaneous(); 19 20 21 printf("\n*************** 文件信息获取测试 **************\r\n"); 22 res_flash = file_check(); 23 24 25 printf("***************** 文件扫描测试 ****************\r\n"); 26 strcpy(fpath,"1:"); 27 scan_files(fpath); 28 29 30 /* 不再使用文件系统,取消挂载文件系统 */ 31 f_mount(NULL,"1:",1); 32 33 /* 操作完成,停机 */ 34 while (1) { 35 } 36 } 串口在程序调试中经常使用,可以把变量值直观打印到串口调试助手,这个信息非常 重要,同样在使用之前需要调用 Debug_USART_Config 函数完成调试串口初始化。 使用 FatFs 进行文件操作之前都使用 f_mount 函数挂载物理设备,这里我们使用 SPI Flash 芯片上的 FAT 文件系统。 接下来我们直接调用 miscellaneous 函数进行 FatFs 设备信息获取、文件定位和格式化 写入功能以及目录创建和重命名功能测试。调用 file_check 函数进行文件信息获取测试。 scan_files 函数用来扫描路径下的所有文件,fpath 是我们定义的一个包含 100 个元素的 字符型数组,并将其赋值为 SPI Flash 芯片物理编号对于的根目录。这样允许 scan_files 函 第 293 页 共 928 零死角玩转 STM32—F429 数见打印 SPI Flash 芯片内 FatFs 所有文件到串口调试助手。注意,这里的定义 fpaht 数组是 必不可少的,因为 scan_files 函数本身是个递归函数,要求实际参数有较大空间的缓存区。 25.4.3 下载验证 保证开发板相关硬件连接正确,用 USB 线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板。程序开始运行,在串口调试 助手可看到每个阶段测试相关信息情况。 25.5 每课一问 1、使用 FatFs 功能编程,在“1:每课一问/测试文件.txt”文本文件的第 20 字节开始写入 一下内容:今天是个好日子,我学会了 FatFs。要求如果文件不存在就新建文件,如果 文件存在就修改文件。最后在串口调试助手打印调试结果。 第 294 页 共 928 零死角玩转 STM32—F429 第26章 FMC—扩展外部 SDRAM 本章参考资料:《STM32F4xx 参考手册 2》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 关于 SDRAM 存储器,请参考前面的“常用存储器介绍”章节,实验中 SDRAM 芯片 的具体参数,请参考其规格书《IS42-45S16400J》来了解。 26.1 SDRAM 控制原理 STM32 控制器芯片内部有一定大小的 SRAM 及 FLASH 作为内存和程序存储空间,但 当程序较大,内存和程序空间不足时,就需要在 STM32 芯片的外部扩展存储器了。 STM32F429 系列芯片扩展内存时可以选择 SRAM 和 SDRAM,由于 SDRAM 的“容量 /价格”比较高,即使用 SDRAM 要比 SRAM 要划算得多。我们以 SDRAM 为例讲解如何 为 STM32 扩展内存。 给 STM32 芯片扩展内存与给 PC 扩展内存的原理是一样的,只是 PC 上一般以内存条 的形式扩展,内存条实质是由多个内存颗粒(即 SDRAM 芯片)组成的通用标准模块,而 STM32 直接与 SDRAM 芯片连接。见图 26-1,这是一种型号为 MT48LC4M32B2 的 SDRAM 芯片内部结构框图,以它为模型进行学习。 图 26-1 一种 SDRAM 芯片的内部结构框图 26.1.1 SDRAM 信号线 图 26-1 虚线框外引出的是 SDRAM 芯片的控制引脚,其说明见表 26-1。 第 295 页 共 928 零死角玩转 STM32—F429 表 26-1 SDRAM 控制引脚说明 信号线 类型 说明 CLK I CKE I CS# I CAS# I RAS# I WE# I DQM[0:3] I BA[0:1] I A[0:11] I DQ[0:31] I/O 同步时钟信号,所有输入信号都在 CLK 为上升沿的时候被采集 时钟使能信号,禁止时钟信号时 SDRAM 会启动自刷新操作 片选信号,低电平有效 列地址选通,为低电平时地址线表示的是列地址 行地址选通,为低电平时地址线表示的是行地址 写入使能,低电平有效 数据输入/输出掩码信号,表示 DQ 信号线的有效部分 Bank 地址输入,选择要控制的 Bank 地址输入 数据输入输出信号 除了时钟、地址和数据线,控制 SDRAM 还需要很多信号配合,它们具体作用在描述 时序图时进行讲解。 26.1.2 控制逻辑 SDRAM 内部的“控制逻辑”指挥着整个系统的运行,外部可通过 CS、WE、CAS、 RAS 以及地址线来向控制逻辑输入命令,命令经过“命令器译码器”译码,并将控制参数 保存到“模式寄存器中”,控制逻辑依此运行。 26.1.3 地址控制 SDRAM 包含有“A”以及“BA”两类地址线,A 类地址线是行(Row)与列(Column)共 用的地址总线,BA 地址线是独立的用于指定 SDRAM 内部存储阵列号(Bank)。在命令模式 下,A 类地址线还用于某些命令输入参数。 26.1.4 SDRAM 的存储阵列 要了解 SDRAM 的储存单元寻址以及“A”、“BA”线的具体运用,需要先熟悉它内 部存储阵列的结构,见图 26-2。 图 26-2 SDRAM 存储阵列模型 第 296 页 共 928 零死角玩转 STM32—F429 SDRAM 内部包含的存储阵列,可以把它理解成一张表格,数据就填在这张表格上。 和表格查找一样,指定一个行地址和列地址,就可以精确地找到目标单元格,这是 SDRAM 芯片寻址的基本原理。这样的每个单元格被称为存储单元,而这样的表则被称为 存储阵列(Bank),目前设计的 SDRAM 芯片基本上内部都包含有 4 个这样的 Bank,寻址时 指定 Bank 号以及行地址,然后再指定列地址即可寻找到目标存储单元。SDRAM 内部具有 多个 Bank 时的结构见图 26-3。 图 26-3 SDRAM 内有多个 Bank 时的结构图 SDRAM 芯片向外部提供有独立的 BA 类地址线用于 Bank 寻址,而行与列则共用 A 类 地址线。 图 26-1 标号中表示的就是它内部的存储阵列结构,通讯时当 RAS 线为低电平,则 “行地址选通器”被选通,地址线 A[11:0]表示的地址会被输入到“行地址译码及锁存器” 中,作为存储阵列中选定的行地址,同时地址线 BA[1:0]表示的 Bank 也被锁存,选中了要 操作的 Bank 号;接着控制 CAS 线为低电平,“列地址选通器”被选通,地址线 A[11:0]表 示的地址会被锁存到“列地址译码器”中作为列地址,完成寻址过程。 26.1.5 数据输入输出 若是写 SDRAM 内容,寻址完成后,DQ[31:0]线表示的数据经过图 26-1 标号中的输 入数据寄存器,然后传输到存储器阵列中,数据被保存;数据输出过程相反。 第 297 页 共 928 零死角玩转 STM32—F429 本型号的 SDRAM 存储阵列的“数据宽度”是 32 位(即数据线的数量),在与 SDRAM 进行数据通讯时,32 位的数据是同步传输的,但实际应用中我们可能会以 8 位、16 位、24 位及 32 位的宽度存取数据,也就是说 32 位的数据线并不是所有时候都同时使用的,而且 在传输低宽度数据的时候,我们不希望其它数据线表示的数据被录入。如传输 8 位数据的 时候,我们只需要 DQ[7:0]表示的数据,而 DQ[31:8]数据线表示的数据必须忽略,否则会 修改非目标存储空间的内容。所以数据输入输出时,还会使用 DQM[3:0]线来配合,每根 DQM 线对应 8 位数据,如“DQM0”为低电平,DQM[1:3]为高电平时,数据线 DQ[7:0]表 示的数据有效,而 DQ[31:8]表示的数据无效。 26.1.6 SDRAM 的命令 控制 SDRAM 需要用到一系列的命令,见表 26-2。各种信号线状态组合产生不同的控 制命令。 表 26-2 SDRAM 命令表 命令名 CS# RAS# CAS# WE# COMMAND INHIBIT HX X X NO OPERATION LH H H ACTIVE LL H H READ LH L H WRITE LH L L PRECHARGE LL H L AUTO REFRESH or SELF REFRESH L L L H LOAD MODE REGISTER LL L L BURST TERMINATE LH H L 表中的 H 表示高电平,L 表示低电平,X 表示任意电平,High-Z 表示高阻态。 DQM X X X L/H L/H X X X X ADDR X X Bank/row Bank/col Bank/col Code X Op-code X DQ X X X X Valid X X X active 1. 命令禁止 只要 CS 引脚为高电平,即表示“命令禁止”(COMMAND INHBIT),它用于禁止 SDRAM 执行新的命令,但它不能停止当前正在执行的命令。 2. 空操作 “空操作”(NO OPERATION),“命令禁止”的反操作,用于选中 SDRAM,以便接 下来发送命令。 3. 行有效 进行存储单元寻址时,需要先选中要访问的 Bank 和行,使它处于激活状态。该操作 通过“行有效”(ACTIVE)命令实现,见图 26-4,发送行有效命令时,RAS 线为低电平, 同时通过 BA 线以及 A 线发送 Bank 地址和行地址。 第 298 页 共 928 零死角玩转 STM32—F429 图 26-4 行有效命令时序图 4. 列读写 行地址通过“行有效”命令确定后,就要对列地址进行寻址了。“读命令”(READ)和 “写命令”(WRITE)的时序很相似,见图 26-5,通过共用的地址线 A 发送列地址,同时使 用 WE 引脚表示读/写方向,WE 为低电平时表示写,高电平时表示读。数据读写时,使用 DQM 线表示有效的 DQ 数据线。 图 26-5 读取命令时序 第 299 页 共 928 零死角玩转 STM32—F429 本型号的 SDRAM 芯片表示列地址时仅使用 A[7:0]线,而 A10 线用于控制是否“自动 预充电”,该线为高电平时使能,低电平时关闭。 5. 预充电 SDRAM 的寻址具有独占性,所以在进行完读写操作后,如果要对同一个 Bank 的另 一行进行寻址,就要将原来有效(ACTIVE)的行关闭,重新发送行/列地址。Bank 关闭当 前工作行,准备打开新行的操作就是预充电(Precharge)。 预充电可以通过独立的命令控制,也可以在每次发送读写命令的同时使用“A10”线 控制自动进行预充电。实际上,预充电是一种对工作行中所有存储阵列进行数据重写,并 对行地址进行复位,以准备新行的工作。 独立的预充电命令时序见图 26-6。该命令配合使用 A10 线控制,若 A10 为高电平时, 所有 Bank 都预充电;A10 为低电平时,使用 BA 线选择要预充电的 Bank。 图 26-6 PRECHARGE 命令时序 6. 刷新 SDRAM 要不断进行刷新(Refresh)才能保留住数据,因此它是 DRAM 最重要的操作。 刷新操作与预充电中重写的操作本质是一样的。 但因为预充电是对一个或所有 Bank 中的工作行操作,并且不定期,而刷新则是有固 定的周期,依次对所有行进行操作,以保证那些久久没被访问的存储单元数据正确。 第 300 页 共 928 零死角玩转 STM32—F429 刷新操作分为两种:“自动刷新”(Auto Refresh)与“自我刷新”(Self Refresh),发 送命令后 CKE 时钟为有效时(低电平),使用自动刷新操作,否则使用自我刷新操作。不论 是何种刷新方式,都不需要外部提供行地址信息,因为这是一个内部的自动操作。 对于“自动刷新”, SDRAM 内部有一个行地址生成器(也称刷新计数器)用来自动 地依次生成行地址,每收到一次命令刷新一行。在刷新过程中,所有 Bank 都停止工作, 而每次刷新所占用的时间为 N 个时钟周期(视 SDRAM 型号而定,通常为 N=9),刷新结束 之后才可进入正常的工作状态,也就是说在这 N 个时钟期间内,所有工作指令只能等待而 无法执行。一次次地按行刷新,刷新完所有行后,将再次对第一行重新进行刷新操作,这 个对同一行刷新操作的时间间隔,称为 SDRAM 的刷新周期,通常为 64ms。显然刷新会对 SDRAM 的性能造成影响,但这是它的 DRAM 的特性决定的,也是 DRAM 相对于 SRAM 取得成本优势的同时所付出的代价。 “自我刷新”则主要用于休眠模式低功耗状态下的数据保存,也就是说即使外部控制 器不工作了,SDRAM 都能自己确保数据正常。在发出“自我刷新”命令后,将 CKE 置于 无效状态(低电平),就进入自我刷新模式,此时不再依靠外部时钟工作,而是根据 SDRAM 内部的时钟进行刷新操作。在自我刷新期间除了 CKE 之外的所有外部信号都是无效的,只 有重新使 CKE 有效才能退出自我刷新模式并进入正常操作状态。 7. 加载模式寄存器 前面提到 SDRAM 的控制逻辑是根据它的模式寄存器来管理整个系统的,而这个寄存 器的参数就是通过“加载模式寄存器”命令(LOAD MODE REGISTER)来配置的。发送该 命令时,使用地址线表示要存入模式寄存器的参数“OP-Code”,各个地址线表示的参数 见图 26-7。 第 301 页 共 928 零死角玩转 STM32—F429 图 26-7 模式寄存器解析图 模式寄存器的各个参数介绍如下: Burst Length Burst Length 译为突发长度,下面简称 BL。突发是指在同一行中相邻的存储单元连续 进行数据传输的方式,连续传输所涉及到存储单元(列)的数量就是突发长度。 上文讲到的读/写操作,都是一次对一个存储单元进行寻址,如果要连续读/写就还要 对当前存储单元的下一个单元进行寻址,也就是要不断的发送列地址与读/写命令(行地址 不变,所以不用再对行寻址)。虽然由于读/写延迟相同可以让数据的传输在 I/O 端是连续 的,但它占用了大量的内存控制资源,在数据进行连续传输时无法输入新的命令,效率很 低。 为此,人们开发了突发传输技术,只要指定起始列地址与突发长度,内存就会依次地 自动对后面相应数量的存储单元进行读/写操作而不再需要控制器连续地提供列地址。这样, 第 302 页 共 928 零死角玩转 STM32—F429 除了第一笔数据的传输需要若干个周期外,其后每个数据只需一个周期的即可获得。其实 我们在 EERPOM 及 FLASH 读写章节讲解的按页写入就是突发写入,而它们的读取过程都 是突发性质的。 非突发连续读取模式:不采用突发传输而是依次单独寻址,此时可等效于 BL=1。虽 然也可以让数据连续地传输,但每次都要发送列地址与命令信息,控制资源占用极大。突 发连续读取模式:只要指定起始列地址与突发长度,寻址与数据的读取自动进行,而只要 控制好两段突发读取命令的间隔周期(与 BL 相同)即可做到连续的突发传输。 而 BL 的数值, 也是不能随便设或在数据进行传输前临时决定。在初始化 SDRAM 调用 LOAD MODE REGISTER 命令时就被固定。BL 可用的选项是 1、2、4、8,常见的设定是 4 和 8。若传输 时实际需要数据长度小于设定的 BL 值,则调用“突发停止”(BURST TERMINATE)命令 结束传输。 BT 模式寄存器中的 BT 位用于设置突发模式,突发模式分为顺序(Sequential)与间隔 (Interleaved)两种。在顺序方式中,操作按地址的顺序连续执行,如果是间隔模式,则操作 地址是跳跃的。跳跃访问的方式比较乱,不太符合思维习惯,我们一般用顺序模式。顺序 访问模式时按照 “0-1-2-3-4-5-6-7”的地址序列访问。 CASLatency 模式寄存器中的 CASLatency 是指列地址选通延迟,简称 CL。在发出读命令(命令同时 包含列地址)后,需要等待几个时钟周期数据线 DQ 才会输出有效数据,这之间的时钟周期 就是指 CL,CL 一般可以设置为 2 或 3 个时钟周期,见图 26-8。 图 26-8 CL=2 和 CL=3 的说明图 CL 只是针对读命令时的数据延时,在写命令是不需要这个延时的,发出写命令时可同 时发送要写入的数据。 第 303 页 共 928 零死角玩转 STM32—F429 Op Mode OP Mode 指 Operating Mode,SDRAM 的工作模式。当它被配置为“00”的时候表示 工作在正常模式,其它值是测试模式或被保留的设定。实际使用时必须配置成正常模式。 WB WB 用于配置写操作的突发特性,可选择使用 BL 设置的突发长度或非突发模式。 Reserved 模式寄存器的最后三位的被保留,没有设置参数。 26.1.7 SDRAM 的初始化流程 最后我们来了解 SDRAM 的初始化流程。SDRAM 并不是上电后立即就可以开始读写 数据的,它需要按步骤进行初始化,对存储矩阵进行预充电、刷新并设置模式寄存器,见 图 26-9。 图 26-9 SDRAM 初始化流程 该流程说明如下: (1) 给 SDRAM 上电,并提供稳定的时钟,至少 100us; (2) 发送“空操作”(NOP)命令; (3) 发送“预充电”(PRECHARGE)命令,控制所有 Bank 进行预充电,并等待 tRP 时间, tRP 表示预充电与其它命令之间的延迟; (4) 发送至少 2 个“自动刷新”(AUTO REFRESH)命令,每个命令后需等待 tRFC 时间,tRFC 表示自动刷新时间; (5) 发送“加载模式寄存器”(LOAD MODE REGISTER)命令,配置 SDRAM 的工作参数, 并等待 tMRD 时间,tMRD 表示加载模式寄存器命令与行有行或刷新命令之间的延迟; (6) 初始化流程完毕,可以开始读写数据。 第 304 页 共 928 零死角玩转 STM32—F429 其中 tRP、tRFC、tMRD 等时间参数跟具体的 SDRAM 有关,可查阅其数据手册获知, STM32 FMC 访问时配置需要这些参数。 26.1.8 SDRAM 的读写流程 初始化步骤完成,开始读写数据,其时序流程见图 26-10 及图 26-11。 图 26-10 CL=2 时,带 AUTO PRECHARGE 的读时序 图 26-11 带 AUTO PRECHARGE 命令的写时序 读时序和写时序的命令过程很类似,下面我们统一解说: 第 305 页 共 928 零死角玩转 STM32—F429 (1) 发送“行有效”(ACTIVE)命令,发送命令的同时包含行地址和 Bank 地址,然后 等待 tRCD 时间,tRCD 表示行有效命令与读/写命令之间的延迟; (2) 发送“读/写”(READ/WRITE)命令,在发送命令的同时发送列地址,完成寻址的 地址输入。对于读命令,根据模式寄存器的 CL 定义,延迟 CL 个时钟周期后, SDRAM 的数据线 DQ 才输出有效数据,而写命令是没有 CL 延迟的,主机在发送 写命令的同时就可以把要写入的数据用 DQ 输入到 SDRAM 中,这是读命令与写 命令的时序最主要的区别。图中的读/写命令都通过地址线 A10 控制自动预充电, 而 SDRAM 接收到带预充电要求的读/写命令后,并不会立即预充电,而是等待 tWR 时间才开始,tWR 表示写命令与预充电之间的延迟; (3) 执行“预充电”(auto precharge)命令后,需要等待 tRP 时间,tRP 表示预充电与其它 命令之间的延迟; (4) 图中的标号处的 tRAS,表示自刷新周期,即在前一个“行有效”与 “预充电” 命令之间的时间; (5) 发送第二次“行有效”(ACTIVE)命令准备读写下一个数据,在图中的标号处的 tRC,表示两个行有效命令或两个刷新命令之间的延迟。 其中 tRCD、tWR、tRP、tRAS 以及 tRC 等时间参数跟具体的 SDRAM 有关,可查阅其数据 手册获知,STM32 FMC 访问时配置需要这些参数。 26.2 FMC 简介 STM32F429 使用 FMC 外设来管理扩展的存储器,FMC 是 Flexible Memory Controller 的缩写,译为可变存储控制器。它可以用于驱动包括 SRAM、SDRAM、NOR FLASH 以及 NAND FLSAH 类型的存储器。在其它系列的 STM32 控制器中,只有 FSMC 控制器 (Flexible Static Memory Controller),译为可变静态存储控制器,所以它们不能驱动 SDRAM 这样的动态存储器,因为驱动 SDRAM 时需要定时刷新,STM32F429 的 FMC 外设才支持 该功能,且只支持普通的 SDRAM,不支持 DDR 类型的 SDRAM。我们只讲述 FMC 的 SDRAM 控制功能。 26.3 FMC 框图剖析 STM32 的 FMC 外设内部结构见图 26-12。 第 306 页 共 928 零死角玩转 STM32—F429 图 26-12 FMC 控制器框图 1. 通讯引脚 在框图的右侧是 FMC 外设相关的控制引脚,由于控制不同类型存储器的时候会有一 些不同的引脚,看起来有非常多,其中地址线 FMC_A 和数据线 FMC_D 是所有控制器都 共用的。这些 FMC 引脚具体对应的 GPIO 端口及引脚号可在《STM32F4xx 规格书》中搜 索查找到,不在此列出。针对 SDRAM 控制器,我们是整理出以下的 FMC 与 SDRAM 引 脚对照表 26-3。 表 26-3 FMC 中的 SDRAM 控制信号线 FMC 引脚名称 FMC_NBL[3:0] FMC_A[12:0] FMC_A[15:14] FMC_D[31:0] FMC_SDCLK FMC_SDNWE 对应 SDRAM 引脚名 DQM[3:0] A[12:0] BA[1:0] DQ[31:0] CLK WE# 说明 数据掩码信号 行/列地址线 Bank 地址线 数据线 同步时钟信号 写入使能 第 307 页 共 928 零死角玩转 STM32—F429 FMC_SDCKE[1:0] CKE FMC_SDNE[1:0] -- FMC_NRAS FMC_NCAS RAS# CAS# SDCKE0:SDRAM 存储区域 1 时钟使能 SDCKE1:SDRAM 存储区域 2 时钟使能 SDNE0:SDRAM 存储区域 1 芯片使能 SDNE1:SDRAM 存储区域 2 芯片使能 行地址选通信号 列地址选通信号 其中比较特殊的是 FMC_A[15:14]引脚用作 Bank 的寻址线;而 FMC_SDCKE 线和 FMC_SDNE 都有 2 条,且 FMC_SDNE 不属于 SDRAM 芯片的控制信号线,它们用于控制 STM32 使用不同的存储区域,FMC_SDCKE 控制存储区域的时钟使能,FMC_SDNE 控制 存储区域的芯片使能。使用不同存储区域时,STM32 访问 SDRAM 的地址不一样,具体将 在“FMC 的地址映射”小节讲解。 2. 存储器控制器 上面不同类型的引脚是连接到 FMC 内部对应的存储控制器中的。NOR/PSRAM/SRAM 设备使用相同的控制器,NAND/PC 卡设备使用相同的控制器,而 SDRAM 存储器使用独 立的控制器。不同的控制器有专用的寄存器用于配置其工作模式。 控制 SDRAM 的有 FMC_SDCR1/FMC_SDCR2 控制寄存器、 FMC_SDTR1/FMC_SDTR2 时序寄存器、FMC_SDCMR 命令模式寄存器以及 FMC_SDRTR 刷新定时器寄存器。其中控制寄存器及时序寄存器各有 2 个,分别对应于 SDRAM 存储区 域 1 和存储区域 2 的配置。 FMC_SDCR 控制寄存器可配置 SDCLK 的同步时钟频率、突发读使能、写保护、CAS 延迟、行列地址位数以及数据总线宽度等。 FMC_SDTR 时序寄存器用于配置 SDRAM 访问时的各种时间延迟,如 TRP 行预充电 延迟、TMRD 加载模式寄存器激活延迟等。 FMC_SDCMR 命令模式寄存器用于存储要发送到 SDRAM 模式寄存器的配置,以及要 向 SDRAM 芯片发送的命令。 FMC_SDRTR 用于配置 SDRAM 的自动刷新周期。 3. 时钟控制逻辑 FMC 外设挂载在 AHB3 总线上,时钟信号来自于 HCLK(默认 180MHz),控制器的时 钟输出就是由它分频得到。如 SDRAM 控制器的 FMC_SDCLK 引脚输出的时钟,是用于与 SDRAM 芯片进行同步通讯,它的时钟频率可通过 FMC_SDCR1 寄存器的 SDCLK 位配置, 可以配置为 HCLK 的 1/2 或 1/3,也就是说,与 SDRAM 通讯的同步时钟最高频率为 90MHz。 26.4 FMC 的地址映射 FMC 连接好外部的存储器并初始化后,就可以直接通过访问地址来读写数据,这种地 址访问与 I2C EEPROM、SPI FLASH 的不一样,后两种方式都需要控制 I2C 或 SPI 总线给 存储器发送地址,然后获取数据;在程序里,这个地址和数据都需要分开使用不同的变量 存储,并且访问时还需要使用代码控制发送读写命令。而使用 FMC 外接存储器时,其存 第 308 页 共 928 零死角玩转 STM32—F429 储单元是映射到 STM32 的内部寻址空间的;在程序里,定义一个指向这些地址的指针,然 后就可以通过指针直接修改该存储单元的内容,FMC 外设会自动完成数据访问过程,读写 命令之类的操作不需要程序控制。FMC 的地址映射见图 26-13。 图 26-13 FMC 的地址映射 图中左侧的是 Cortex-M4 内核的存储空间分配,右侧是 STM32 FMC 外设的地址映射。 可以看到 FMC 的 NOR/PSRAM/SRAM/NAND FLASH 以及 PC 卡的地址都在 External RAM 地址空间内,而 SDRAM 的地址是分配到 External device 区域的。正是因为存在这样的地 址映射,使得访问 FMC 控制的存储器时,就跟访问 STM32 的片上外设寄存器一样(片上外 设的地址映射即图中左侧的“Peripheral”区域)。 1. External RAM 与 External device 的区别 比较遗憾的是 FMC 给 SDRAM 分配的区域不在 External RAM 区,这个区域可以直接 执行代码,而 SDRAM 所在的 External device 区却不支持这个功能。这里说的可直接执行 代码的特性就是在“常用存储器”章节介绍的 XIP(eXecute In Place)特性,即存储器上若存 储了代码,CPU 可直接访问代码执行,无需缓存到其它设备上再运行;而且 XIP 特性还对 存储器的种类有要求,SRAM/SDRAM 及 NOR Flash 都支持这种特性,而 NAND FLASH 及 PC 卡是不支持 XIP 的。结合存储器的特性和 STM32 FMC 存储器种类的地址分配,就 发现它的地址规划不合理了,NAND FLASH 和 PC 卡这些不支持 XIP 的存储器却占据了 External RAM 的空间,而支持 XIP 的 SDRAM 存储器的空间却被分配到了 Extern device 区。 第 309 页 共 928 零死角玩转 STM32—F429 为了解决这个问题,通过配置“SYSCFG_MEMRMP”寄存器的“SWP_FMC”寄存器位可 用于交换 SDRAM 与 NAND/PC 卡的地址映射,使得存储在 SDRAM 中的代码能被执行, 只是由于 SDRAM 的最高同步时钟是 90MHz,代码的执行速度会受影响。 本章主要讲解当 STM32 的片内 SRAM 不够用时使用 SDRAM 扩展内存,但假如程序 太大,它的程序空间 FLASH 不够用怎么办呢?首先是裁剪代码,目前 STM32F429 系列芯 片内部 FLASH 空间最高可达 2MB,实际应用中只要我们把代码中的图片、字模等占据大 空间的内容放到外部存储器中,纯粹的代码很难达到 2MB。如果还不够用,非要扩展程序 空间的话,一种方法是使用 FMC 扩展 NOR FLASH,把程序存储到 NOR 上,程序代码能 够直接在 NOR FLASH 上执行。另一种方法是把程序存储在其它外部存储器,如 SD 卡, 需要时把存储在 SD 卡上的代码加载到 SRAM 或 SDRAM 上,再在 RAM 上执行代码。 如果 SDRAM 不是用于存储可执行代码,只是用来保存数据的话,在 External RAM 或 Exteranl device 区域都没有区别,不需要与 NAND 的映射地址交换。 2. SDRAM 的存储区域 FMC 把 SDRAM 的存储区域分成了 Bank1 和 Bank2 两块,这里的 Bank 与 SDRAM 芯 片内部的 Bank 是不一样的概念,只是 FMC 的地址区域划分而已。每个 Bank 有不一样的 起始地址,且有独立的 FMC_SDCR 控制寄存器和 FMC_SDTR 时序寄存器,还有独立的 FMC_SDCKE 时钟使能信号线和 FMC_SDCLK 信号线。FMC_SDCKE0 和 FMC_SDCLK0 对应的存储区域 1 的地址范围是 0xC000 0000-0xCFFF FFFF,而 FMC_SDCKE1 和 FMC_SDCLK1 对应的存储区域 2 的地址范围是 0xD000 0000- 0xDFFF FFFF。 26.5 SDRAM 时序结构体 控制 FMC 使用 SDRAM 存储器时主要是配置时序寄存器以及控制寄存器,利用 ST 标 准库的 SDRAM 时序结构体以及初始化结构体可以很方便地写入参数。 SDRAM 时序结构体的成员见代码清单 24-1。 代码清单 26-1 SDRAM 时序结构体 FMC_SDRAMTimingInitTypeDef 1 /* @brief 控制 SDRAM 的时序参数,这些参数的单位都是“周期” 2* 各个参数的值可设置为 1-16 个周期。 */ 3 typedef struct 4{ 5 uint32_t FMC_LoadToActiveDelay; /*TMRD:加载模式寄存器命令后的延迟*/ 6 uint32_t FMC_ExitSelfRefreshDelay; /*TXSR:自刷新命令后的延迟 */ 7 uint32_t FMC_SelfRefreshTime; /*TRAS:自刷新时间*/ 8 uint32_t FMC_RowCycleDelay; /*TRC:行循环延迟*/ 9 uint32_t FMC_WriteRecoveryTime; /*TWR:恢复延迟 */ 10 uint32_t FMC_RPDelay; /*TRP:行预充电延迟*/ 11 uint32_t FMC_RCDDelay; /*TRCD:行到列延迟*/ 12 } FMC_SDRAMTimingInitTypeDef; 这个结构体成员定义的都是 SDRAM 发送各种命令后必须的延迟,它的配置对应到 FMC_SDTR 中的寄存器位。所有成员参数值的单位是周期,参数值大小都可设置成“1- 16”。关于这些延时时间的定义可以看“SDRAM 初始化流程”和“SDRAM 读写流程”小 节的时序图了解。具体参数值根据 SDRAM 芯片的手册说明来配置。各成员介绍如下: 第 310 页 共 928 零死角玩转 STM32—F429 (1) FMC_LoadToActiveDelay 本成员设置 TMRD 延迟(Load Mode Register to Active),即发送加载模式寄存器命令后 要等待的时间,过了这段时间才可以发送行有效或刷新命令。 (2) FMC_ExitSelfRefreshDelay 本成员设置退出 TXSR 延迟(Exit Self-refresh delay),即退出自我刷新命令后要等待的 时间,过了这段时间才可以发送行有效命令。 (3) FMC_SelfRefreshTime 本成员设置自我刷新时间 TRAS,即发送行有效命令后要等待的时间,过了这段时间 才执行预充电命令。 (4) FMC_RowCycleDelay 本成员设置 TRC 延迟(Row cycle delay),即两个行有效命令之间的延迟,以及两个相 邻刷新命令之间的延迟 (5) FMC_WriteRecoveryTime 本成员设置 TWR 延迟(Recovery delay),即写命令和预充电命令之间的延迟,等待这 段时间后才开始执行预充电命令。 (6) FMC_RPDelay 本成员设置 TRP 延迟(Row precharge delay),即预充电命令与其它命令之间的延迟。 (7) FMC_RCDDelay 本成员设置 TRCD 延迟(Row to column delay),即行有效命令到列读写命令之间的延迟。 这个 SDRAMTimingInitTypeDef 时序结构体配置的延时参数,将作为下一节的 FMC SDRAM 初始化结构体的一个成员。 26.6 SDRAM 初始化结构体 FMC 的 SDRAM 初始化结构体见代码清单 26-2。 代码清单 26-2 SDRAM 初始化结构体 FMC_SDRAMInitTypeDef 1 /* @brief FMC SDRAM 初始化结构体类型定义 */ 2 typedef struct 3{ 4 uint32_t FMC_Bank; /*选择 FMC 的 SDRAM 存储区域*/ 5 uint32_t FMC_ColumnBitsNumber; /*定义 SDRAM 的列地址宽度 */ 6 uint32_t FMC_RowBitsNumber; /*定义 SDRAM 的行地址宽度 */ 7 uint32_t FMC_SDMemoryDataWidth; /*定义 SDRAM 的数据宽度 */ 8 uint32_t FMC_InternalBankNumber; /*定义 SDRAM 内部的 Bank 数目 */ 9 uint32_t FMC_CASLatency; /*定义 CASLatency 的时钟个数*/ 10 uint32_t FMC_WriteProtection; /*定义是否使能写保护模式 */ 11 uint32_t FMC_SDClockPeriod; /*配置同步时钟 SDCLK 的参数*/ 12 uint32_t FMC_ReadBurst; /*是否使能突发读模式*/ 13 uint32_t FMC_ReadPipeDelay; /*定义在 CAS 个延迟后再等待多 14 少个 HCLK 时钟才读取数据 */ 15 FMC_SDRAMTimingInitTypeDef* FMC_SDRAMTimingStruct; /*定义 SDRAM 的时序参数 */ 16 } FMC_SDRAMInitTypeDef; 第 311 页 共 928 零死角玩转 STM32—F429 这个结构体,除最后一个成员是上一小节讲解的时序配置外,其它结构体成员的配置 都对应到 FMC_SDCR 中的寄存器位。各个成员意义在前面的小节已有具体讲解,其可选 参数介绍如下,括号中的是 STM32 标准库定义的宏: (1) FMC_Bank 本成员用于选择 FMC 映射的 SDRAM 存储区域,可选择存储区域 1 或 2 (FMC_Bank1/2_SDRAM)。 (2) FMC_ColumnBitsNumber 本成员用于设置要控制的 SDRAM 的列地址宽度,可选择 8-11 位 (FMC_ColumnBits_Number_8/9/10/11b)。 (3) FMC_RowBitsNumber 本成员用于设置要控制的 SDRAM 的行地址宽度,可选择设置成 11-13 位 (FMC_RowBits_Number_11/12/13b)。 (4) FMC_SDMemoryDataWidth 本成员用于设置要控制的 SDRAM 的数据宽度,可选择设置成 8、16 或 32 位 (FMC_SDMemory_Width_8/16/32b)。 (5) FMC_InternalBankNumber 本成员用于设置要控制的 SDRAM 的内部 Bank 数目,可选择设置成 2 或 4 个 Bank 数 目(FMC_InternalBank_Number_2/4),请注意区分这个结构体成员与 FMC_Bank 的区别。 (6) FMC_CASLatency 本成员用于设置 CASLatency 即 CL 的时钟数目,可选择设置为 1、2 或 3 个时钟周期 (FMC_CAS_Latency_1/2/3)。 (7) FMC_WriteProtection 本成员用于设置是否使能写保护模式,如果使能了写保护则不能向 SDRAM 写入数据, 正常使用都是禁止写保护的。 (8) FMC_SDClockPeriod 本成员用于设置 FMC 与外部 SDRAM 通讯时的同步时钟参数,可以设置成 STM32 的 HCLK 时 钟 频 率 的 1/2 、 1/3 或 禁 止 输 出 时 钟 (FMC_SDClock_Period_2/3 或 FMC_SDClock_Disable)。 (9) FMC_ReadBurst 本成员用于设置是否使能突发读取模式,禁止时等效于 BL=1,使能时 BL 的值等于模 式寄存器中的配置。 (10) FMC_ReadPipeDelay 本成员用于配置在 CASLatency 个时钟周期后,再等待多少个 HCLK 时钟周期才进行 数据采样,在确保正确的前提下,这个值设置为越短越好,可选择设置的参数值为 0、 1 或 2 个 HCLK 时钟周期(FMC_ReadPipe_Delay_0/1/2)。 (11) FMC_SDRAMTimingStruct 这个成员就是我们上一小节讲解的 SDRAM 时序结构体了,设置完时序结构体后再把 赋值到这里即可。 第 312 页 共 928 零死角玩转 STM32—F429 配置完 SDRAM 初始化结构体后,调用 FMC_SDRAMInit 函数把这些配置写入到 FMC 的 SDRAM 控制寄存器及时序寄存器,实现 FMC 的初始化。 26.7 SDRAM 命令结构体 控制 SDRAM 时需要各种命令,通过向 FMC 的命令模式寄存器 FMC_SDCMR 写入控 制参数,即可控制 FMC 对外发送命令,为了方便使用,STM32 标准库也把它封装成了结 构体,见代码清单 26-3。 代码清单 26-3 SDRAM 命令结构体 1 typedef struct 2{ 3 uint32_t FMC_CommandMode; /*要发送的命令 */ 4 uint32_t FMC_CommandTarget; /*目标存储器区域 */ 5 uint32_t FMC_AutoRefreshNumber; /*若发送的是自动刷新命令, 6 此处为发送的刷新次数,其它命 令时无效 */ 7 uint32_t FMC_ModeRegisterDefinition; /*若发送的是加载模式寄存器命令, 8 此处为要写入 SDRAM 模式寄存器 的参数 */ 9 } FMC_SDRAMCommandTypeDef; 命令结构体中的各个成员介绍如下: (1) FMC_CommandMode 本成员用于配置将要发送的命令,它可以被赋值为表 26-4 中的宏,这些宏代表了不同 命令; 表 26-4 FMC 可输出的 SDRAM 控制命令 宏 命令说明 FMC_Command_Mode_normal 正常模式命令 FMC_Command_Mode_CLK_Enabled 使能 CLK 命令 FMC_Command_Mode_PALL 对所有 Bank 预充电命令 FMC_Command_Mode_AutoRefresh 自动刷新命令 FMC_Command_Mode_LoadMode 加载模式寄存器命令 FMC_Command_Mode_Selfrefresh 自我刷新命令 FMC_Command_Mode_PowerDown 掉电命令 (2) FMC_CommandMode 本成员用于选择要控制的 FMC 存储区域,可选择存储区域 1 或 2(FMC_Command_Target_bank1/2); (3) FMC_AutoRefreshNumber 有时需要连续发送多个 “自动刷新”(Auto Refresh)命令时,配置本成员即可控制它发 送多少次,可输入参数值为 1-16,若发送的是其它命令,本参数值无效。如 FMC_CommandMode 成员被配置为宏 FMC_Command_Mode_AutoRefresh,而 FMC_AutoRefreshNumber 被设置为 2 时,FMC 就会控制发送 2 次自动刷新命令。 (4) FMC_ModeRegisterDefinition 当向 SDRAM 发送加载模式寄存器命令时,这个结构体成员的值将通过地址线发送到 SDRAM 的模式寄存器中,这个成员值长度为 13 位,各个位一一对应 SDRAM 的模式 寄存器。 第 313 页 共 928 零死角玩转 STM32—F429 配置完这些结构体成员,调用库函数 FMC_SDRAMCmdConfig 即可把这些参数写入到 FMC_SDCMR 寄存器中,然后 FMC 外设就会发送相应的命令了。 26.8 FMC—扩展外部 SDRAM 实验 本小节以型号为“IS42S16400J”的 SDRAM 芯片为 STM32 扩展内存。它的行地址宽 度为 12 位,列地址宽度为 8 位,内部含有 4 个 Bank,数据线宽度为 16 位,容量大小为 8MB。 学习本小节内容时,请打开配套的“FMC—读写 SDRAM”工程配合阅读。本实验仅 讲解基本的 SDRAM 驱动,不涉及内存管理的内容,在本书的《MDK 编译过程及文件类型 全解》章节将会讲解使用更简单的方法从 SDRAM 中分配变量,以及使用 C 语言标准库的 malloc 函数来分配 SDRAM 的空间。 26.8.1 硬件设计 图 26-14 SDRAM 硬件连接图 SDRAM 与 STM32 相连的引脚非常多,主要是地址线和数据线,这些具有特定 FMC 功能的 GPIO 引脚可查询《STM32F4xx 规格书》中的说明来了解。 关于该 SDRAM 芯片的更多信息,请参考其规格书《IS42-45S16400J》了解。若您使 用的实验板 FLASH 的型号或控制引脚不一样,可在我们工程的基础上修改,程序的控制 原理相同。 第 314 页 共 928 零死角玩转 STM32—F429 26.8.2 软件设计 为了使工程更加有条理,我们把 SDRAM 初始化相关的代码独立分开存储,方便以后 移植。在“工程模板”之上新建“bsp_sdram.c”及“bsp_sdram.h”文件,这些文件也可根 据您的喜好命名,它们不属于 STM32 标准库的内容,是由我们自己根据应用需要编写的。 1. 编程要点 (13) 初始化通讯使用的目标引脚及端口时钟; (14) 使能 FMC 外设的时钟; (15) 配置 FMC SDRAM 的时序、工作模式; (16) 根据 SDRAM 的初始化流程编写初始化函数; (17) 建立机制访问外部 SDRAM 存储器; (18) 编写测试程序,对读写数据进行校验。 2. 代码分析 FMC 硬件相关宏定义 我们把 FMC SDRAM 硬件相关的配置都以宏的形式定义到 “bsp_sdram.h”文件中, 见代码清单 24-2。 代码清单 26-4 SDRAM 硬件配置相关的宏(省略了大部分数据线) 1 /*A 行列地址信号线*/ 2 #define FMC_A0_GPIO_PORT GPIOF 3 #define FMC_A0_GPIO_CLK RCC_AHB1Periph_GPIOF 4 #define FMC_A0_GPIO_PIN GPIO_Pin_0 5 #define FMC_A0_PINSOURCE GPIO_PinSource0 6 #define FMC_A0_AF GPIO_AF_FMC 7 /*......*/ 8 /*此处省略 A1-A11 信号线的宏,具体可参考工程中的代码*/ 9 /*BA 地址线*/ 10 #define FMC_BA0_GPIO_PORT GPIOG 11 #define FMC_BA0_GPIO_CLK RCC_AHB1Periph_GPIOG 12 #define FMC_BA0_GPIO_PIN GPIO_Pin_4 13 #define FMC_BA0_PINSOURCE GPIO_PinSource4 14 #define FMC_BA0_AF GPIO_AF_FMC 15 /*......*/ 16 /*此处省略 BA1 信号线的宏,具体可参考工程中的代码*/ 17 18 /*DQ 数据信号线*/ 19 #define FMC_D0_GPIO_PORT GPIOD 20 #define FMC_D0_GPIO_CLK RCC_AHB1Periph_GPIOD 21 #define FMC_D0_GPIO_PIN GPIO_Pin_14 22 #define FMC_D0_PINSOURCE GPIO_PinSource14 23 #define FMC_D0_AF GPIO_AF_FMC 24 /*......*/ 25 /*此处省略 D1-A15 信号线的宏,具体可参考工程中的代码*/ 26 27 /*控制信号线*/ 28 /*CS 片选*/ 29 #define FMC_CS_GPIO_PORT GPIOH 30 #define FMC_CS_GPIO_CLK RCC_AHB1Periph_GPIOH 31 #define FMC_CS_GPIO_PIN GPIO_Pin_6 第 315 页 共 928 零死角玩转 STM32—F429 32 #define FMC_CS_PINSOURCE 33 #define FMC_CS_AF 34 /*WE 写使能*/ 35 #define FMC_WE_GPIO_PORT 36 #define FMC_WE_GPIO_CLK 37 #define FMC_WE_GPIO_PIN 38 #define FMC_WE_PINSOURCE 39 #define FMC_WE_AF 40 /*RAS 行选通*/ 41 #define FMC_RAS_GPIO_PORT 42 #define FMC_RAS_GPIO_CLK 43 #define FMC_RAS_GPIO_PIN 44 #define FMC_RAS_PINSOURCE 45 #define FMC_RAS_AF 46 /*CAS 列选通*/ 47 #define FMC_CAS_GPIO_PORT 48 #define FMC_CAS_GPIO_CLK 49 #define FMC_CAS_GPIO_PIN 50 #define FMC_CAS_PINSOURCE 51 #define FMC_CAS_AF 52 /*CLK 同步时钟,存储区域 2*/ 53 #define FMC_CLK_GPIO_PORT 54 #define FMC_CLK_GPIO_CLK 55 #define FMC_CLK_GPIO_PIN 56 #define FMC_CLK_PINSOURCE 57 #define FMC_CLK_AF 58 /*CKE 时钟使能,存储区域 2*/ 59 #define FMC_CKE_GPIO_PORT 60 #define FMC_CKE_GPIO_CLK 61 #define FMC_CKE_GPIO_PIN 62 #define FMC_CKE_PINSOURCE 63 #define FMC_CKE_AF 64 65 /*DQM1 数据掩码*/ 66 #define FMC_UDQM_GPIO_PORT 67 #define FMC_UDQM_GPIO_CLK 68 #define FMC_UDQM_GPIO_PIN 69 #define FMC_UDQM_PINSOURCE 70 #define FMC_UDQM_AF 71 /*DQM0 数据掩码*/ 72 #define FMC_LDQM_GPIO_PORT 73 #define FMC_LDQM_GPIO_CLK 74 #define FMC_LDQM_GPIO_PIN 75 #define FMC_LDQM_PINSOURCE 76 #define FMC_LDQM_AF GPIO_PinSource6 GPIO_AF_FMC GPIOC RCC_AHB1Periph_GPIOC GPIO_Pin_0 GPIO_PinSource0 GPIO_AF_FMC GPIOF RCC_AHB1Periph_GPIOF GPIO_Pin_11 GPIO_PinSource11 GPIO_AF_FMC GPIOG RCC_AHB1Periph_GPIOG GPIO_Pin_15 GPIO_PinSource15 GPIO_AF_FMC GPIOG RCC_AHB1Periph_GPIOG GPIO_Pin_8 GPIO_PinSource8 GPIO_AF_FMC GPIOH RCC_AHB1Periph_GPIOH GPIO_Pin_7 GPIO_PinSource7 GPIO_AF_FMC GPIOE RCC_AHB1Periph_GPIOE GPIO_Pin_1 GPIO_PinSource1 GPIO_AF_FMC GPIOE RCC_AHB1Periph_GPIOE GPIO_Pin_0 GPIO_PinSource0 GPIO_AF_FMC 以上代码根据硬件的连接,把与 SDRAM 通讯使用的引脚号、引脚源以及复用功能映 射都以宏封装起来。其中 FMC_CKE 和 FMC_CLK 引脚对应的是 FMC 的存储区域 2,所以 后面我们对 SDRAM 的寻址空间也是要指向存储区域 2 的。 初始化 FMC 的 GPIO 利用上面的宏,编写 FMC 的 GPIO 引脚初始化函数,见代码清单 24-3。 代码清单 26-5 FMC 的 GPIO 初始化函数(省略了大部分数据线) 1 /** 2 * @brief 初始化控制 SDRAM 的 IO 3 * @param 无 4 * @retval 无 5 */ 6 static void SDRAM_GPIO_Config(void) 7{ 8 GPIO_InitTypeDef GPIO_InitStructure; 9 第 316 页 共 928 零死角玩转 STM32—F429 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 } /*此处省略大量地址线、数据线以及控制信号线, 它们的时钟配置都相同,具体请查看工程中的代码*/ /* 使能 SDRAM 相关的 GPIO 时钟 */ /*地址信号线*/ RCC_AHB1PeriphClockCmd(FMC_A0_GPIO_CLK | /*...*/ /*数据信号线*/ /*控制信号线*/ FMC_D0_GPIO_CLK |FMC_CS_GPIO_CLK | , ENABLE); /*--所有 GPIO 的配置都相同,此处省略大量引脚初始化,具体请查看工程中的代码*/ /* 通用 GPIO 配置 */ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //配置为复用功能 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽输出 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; /*A 行列地址信号线 针对引脚配置*/ GPIO_InitStructure.GPIO_Pin = FMC_A0_GPIO_PIN; GPIO_Init(FMC_A0_GPIO_PORT, &GPIO_InitStructure); GPIO_PinAFConfig(FMC_A0_GPIO_PORT, FMC_A0_PINSOURCE , FMC_A0_AF); /*...*/ /*DQ 数据信号线 针对引脚配置*/ GPIO_InitStructure.GPIO_Pin = FMC_D0_GPIO_PIN; GPIO_Init(FMC_D0_GPIO_PORT, &GPIO_InitStructure); GPIO_PinAFConfig(FMC_D0_GPIO_PORT, FMC_D0_PINSOURCE , FMC_D0_AF); /*...*/ /*控制信号线*/ GPIO_InitStructure.GPIO_Pin = FMC_CS_GPIO_PIN; GPIO_Init(FMC_CS_GPIO_PORT, &GPIO_InitStructure); GPIO_PinAFConfig(FMC_CS_GPIO_PORT, FMC_CS_PINSOURCE , FMC_CS_AF); /*...*/ 与所有使用到 GPIO 的外设一样,都要先把使用到的 GPIO 引脚模式初始化,以上代 码把 FMC SDRAM 的所有信号线全都初始化为 FMC 复用功能,所有引脚配置都是一样的。 配置 FMC 的模式 接下来需要配置 FMC SDRAM 的工作模式,这个函数的主体是根据硬件连接的 SDRAM 特性,对时序结构体以及初始化结构体进行赋值。见代码清单 24-4。 代码清单 26-6 配置 FMC 的模式 1 /** 2 * @brief 初始化配置使用 SDRAM 的 FMC 及 GPIO 接口, 3* 本函数在 SDRAM 读写操作前需要被调用 4 * @param None 5 * @retval None 6 */ 7 void SDRAM_Init(void) 8{ 9 FMC_SDRAMInitTypeDef FMC_SDRAMInitStructure; 10 FMC_SDRAMTimingInitTypeDef FMC_SDRAMTimingInitStructure; 11 12 /* 配置 FMC 接口相关的 GPIO*/ 13 SDRAM_GPIO_Config(); 14 15 /* 使能 FMC 时钟 */ 16 RCC_AHB3PeriphClockCmd(RCC_AHB3Periph_FMC, ENABLE); 17 18 /* SDRAM 时序结构体,根据 SDRAM 参数表配置----------------*/ 19 /* SDCLK: 90 Mhz (HCLK/2 :180Mhz/2) 1 个时钟周期 Tsdclk =1/90MHz=11.11ns*/ 20 /* TMRD: 2 Clock cycles */ 21 FMC_SDRAMTimingInitStructure.FMC_LoadToActiveDelay = 2; 22 /* TXSR: min=70ns (7x11.11ns) */ 第 317 页 共 928 零死角玩转 STM32—F429 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 48 49 50 51 52 53 54 55 56 57 58 59 60 62 63 64 65 66 67 68 } FMC_SDRAMTimingInitStructure.FMC_ExitSelfRefreshDelay = 7; /* TRAS: min=42ns (4x11.11ns) max=120k (ns) */ FMC_SDRAMTimingInitStructure.FMC_SelfRefreshTime = 4; /* TRC: min=70 (7x11.11ns) */ FMC_SDRAMTimingInitStructure.FMC_RowCycleDelay = 7; /* TWR: min=1+ 7ns (1+1x11.11ns) */ FMC_SDRAMTimingInitStructure.FMC_WriteRecoveryTime = 2; /* TRP: 15ns => 2x11.11ns */ FMC_SDRAMTimingInitStructure.FMC_RPDelay = 2; /* TRCD: 15ns => 2x11.11ns */ FMC_SDRAMTimingInitStructure.FMC_RCDDelay = 2; /* FMC SDRAM 控制配置 */ /*选择存储区域*/ FMC_SDRAMInitStructure.FMC_Bank = FMC_Bank2_SDRAM; /* 行地址线宽度: [7:0] */ FMC_SDRAMInitStructure.FMC_ColumnBitsNumber = FMC_ColumnBits_Number_8b; /* 列地址线宽度: [11:0] */ FMC_SDRAMInitStructure.FMC_RowBitsNumber = FMC_RowBits_Number_12b; /* 数据线宽度 */ FMC_SDRAMInitStructure.FMC_SDMemoryDataWidth = SDRAM_MEMORY_WIDTH; /* SDRAM 内部 bank 数量*/ FMC_SDRAMInitStructure.FMC_InternalBankNumber =FMC_InternalBank_Number_4; /* CAS 潜伏期 */ FMC_SDRAMInitStructure.FMC_CASLatency = FMC_CAS_Latency_2; /* 禁止写保护*/ FMC_SDRAMInitStructure.FMC_WriteProtection = FMC_Write_Protection_Disable; /* SDCLK 时钟分频因子,SDCLK = HCLK/SDCLOCK_PERIOD*/ FMC_SDRAMInitStructure.FMC_SDClockPeriod = FMC_SDClock_Period_2; /* 突发读模式设置*/ FMC_SDRAMInitStructure.FMC_ReadBurst = FMC_Read_Burst_Enable; /* 读延迟配置 */ FMC_SDRAMInitStructure.FMC_ReadPipeDelay = FMC_ReadPipe_Delay_0; /* SDRAM 时序参数 */ FMC_SDRAMInitStructure.FMC_SDRAMTimingStruct =&FMC_SDRAMTimingInitStructure; /* 调用初始化函数,向寄存器写入配置 */ FMC_SDRAMInit(&FMC_SDRAMInitStructure); /* 执行 FMC SDRAM 的初始化流程*/ SDRAM_InitSequence(); 这个函数的执行流程如下: (1) 初始化 GPIO 引脚以及 FMC 时钟 函数开头调用了前面定义的 SDRAM_GPIO_Config 函数对 FMC 用到的 GPIO 进行初始 化,并且使用库函数 RCC_AHB3PeriphClockCmd 使能 FMC 外设的时钟。 (2) 时序结构体赋值 接下来对时序结构体 FMC_SDRAMTimingInitStructure 赋值。在前面我们了解到时序 结构体各个成员值的单位是同步时钟 SDCLK 的周期数,而根据我们使用的 SDRAM 芯片, 可查询得它对这些时序要求,见表 26-5。 时间参数 trc tras trp 表 26-5 SDRAM 的延时参数(摘自《IS42-45S16400J》规格书) 说明 最小值 两个刷新命令或两个行有效命令之间的延迟 63 行有效与预充电命令之间的延迟 42 预充电与行有效命令之间的延迟 15 单位 ns ns ns 第 318 页 共 928 零死角玩转 STM32—F429 trcd 行有效与列读写命令之间的延迟 15 ns twr 写入命令到预充电命令之间的延迟 2 cycle txsr 退出自我刷新到行有效命令之间的延迟 70 ns tmrd 加载模式寄存器命令与行有效或刷新命令之间的延迟 2 cycle 部分时间参数以 ns 为单位,因此我们需要进行单位转换,而以 SDCLK 时钟周期数 (cycle)为单位的时间参数,直接赋值到时序结构体成员里即可。 由于我们配置 FMC 输出的 SDCLK 时钟频率为 HCLK 的 1/2(在后面的程序里配置的), 即 FSDCLK=90MHz,可得 1 个 SDCLK 时钟周期长度为 TSDCLK=1/FSDCLK =11.11ns,然后设置 各个成员的时候,只要保证时间大于以上 SDRAM 延时参数表的要求即可。如 trc 要求大于 63ns,而 11.11ns x 7=77.77ns,所以 FMC_RowCycleDelay(TRC)成员值被设置为 7 个时钟周 期,依葫芦画瓢完成时序参数的设置。 (3) 配置 FMC 初始化结构体 函数接下来对 FMC SDRAM 的初始化结构体赋值。包括行列地址线宽度、数据线宽度、 SDRAM 内部 Bank 数量以及 CL 长度,这些都是根据外接的 SDRAM 的特性设置的,其中 CL 长度要与后面初始化流程中给 SDRAM 模式寄存器中的赋值一致。  设置存储区域 FMC_Bank 成员设置 FMC 的 SDRAM 存储区域映射选择为 FMC_Bank2_SDRAM, 这是由于我们的 SDRAM 硬件连接到 FMC_CKE1 和 FMC_CLK1,所以对应到存 储区域 2;  行地址、列地址、数据线宽度及内部 Bank 数量 这些结构体成员都是根据 SDRAM 芯片的特性配置的,行地址宽度为 8 位,列地 址宽度为 12 位,数据线宽度为 16 位,SDRAM 内部有 4 个 Bank;  CL 长度 CL 的长度这里被设置为 2 个同步时钟周期,它需要与后面 SDRAM 模式寄存器中 的配置一样;  写保护 FMC_WriteProtection 用于设置写保护,如果使能了这个功能是无法向 SDRAM 写 入数据的,所以我们关闭这个功能;  同步时钟参数 FMC_SDClockPeriod 成员被设置为 FMC_SDClock_Period_2 ,所以同步时钟的频 率就被设置为 HCLK 的 1/2 了;  突发读模式及读延迟 为了加快读取速度,我们使能突发读功能,且读延迟周期为 0;  时序参数 最后向 FMC_SDRAMTimingStruct 赋值为前面的时序结构体,包含了我们设定的 SDRAM 时间参数。  赋值完成后调用库函数 FMC_SDRAMInit 把初始化结构体配置的各种参数写入到 FMC_SDCR 控 制 寄 存 器 及 FMC_SDTR 时 序 寄 存 器 中 。 函 数 的 最 后 调 用 SDRAM_InitSequence 函数实现执行 SDRAM 的上电初始化时序。 第 319 页 共 928 零死角玩转 STM32—F429 实现 SDRAM 的初始化时序 在上面配置完成 STM32 的 FMC 外设参数后,在读写 SDRAM 前还需要执行前面介绍 的 SDRAM 上电初始化时序,它就是由 SDRAM_InitSequence 函数实现的,见代码清单 44-7。 代码清单 26-7 SDRAM 上电初始化时序 1 /** 2 * @brief 对 SDRAM 芯片进行初始化配置 3 * @param None. 4 * @retval None. 5 */ 6 static void SDRAM_InitSequence(void) 7{ 8 FMC_SDRAMCommandTypeDef FMC_SDRAMCommandStructure; 9 uint32_t tmpr = 0; 10 11 /* Step 3 -----------------------------------------------*/ 12 /* 配置命令:开启提供给 SDRAM 的时钟 */ 13 FMC_SDRAMCommandStructure.FMC_CommandMode = FMC_Command_Mode_CLK_Enabled; 14 FMC_SDRAMCommandStructure.FMC_CommandTarget = FMC_COMMAND_TARGET_BANK; 15 FMC_SDRAMCommandStructure.FMC_AutoRefreshNumber = 1; 16 FMC_SDRAMCommandStructure.FMC_ModeRegisterDefinition = 0; 17 /* 检查 SDRAM 标志,等待至 SDRAM 空闲 */ 18 while (FMC_GetFlagStatus(FMC_BANK_SDRAM, FMC_FLAG_Busy) != RESET); 19 /* 发送上述命令*/ 20 FMC_SDRAMCmdConfig(&FMC_SDRAMCommandStructure); 21 22 /* Step 4 ---------------------------------------------*/ 23 /*延时 */ 24 SDRAM_delay(10); 25 26 /* Step 5 -------------------------------------------*/ 27 /* 配置命令:对所有的 bank 预充电 */ 28 FMC_SDRAMCommandStructure.FMC_CommandMode = FMC_Command_Mode_PALL; 29 FMC_SDRAMCommandStructure.FMC_CommandTarget = FMC_COMMAND_TARGET_BANK; 30 FMC_SDRAMCommandStructure.FMC_AutoRefreshNumber = 1; 31 FMC_SDRAMCommandStructure.FMC_ModeRegisterDefinition = 0; 32 /* 检查 SDRAM 标志,等待至 SDRAM 空闲 */ 33 while (FMC_GetFlagStatus(FMC_BANK_SDRAM, FMC_FLAG_Busy) != RESET); 34 /* 发送上述命令*/ 35 FMC_SDRAMCmdConfig(&FMC_SDRAMCommandStructure); 36 37 /* Step 6 --------------------------------------------*/ 38 /* 配置命令:自动刷新 */ 39 FMC_SDRAMCommandStructure.FMC_CommandMode = FMC_Command_Mode_AutoRefresh; 40 FMC_SDRAMCommandStructure.FMC_CommandTarget = FMC_COMMAND_TARGET_BANK; 41 FMC_SDRAMCommandStructure.FMC_AutoRefreshNumber = 2; 42 FMC_SDRAMCommandStructure.FMC_ModeRegisterDefinition = 0; 43 /* 检查 SDRAM 标志,等待至 SDRAM 空闲 */ 44 while (FMC_GetFlagStatus(FMC_BANK_SDRAM, FMC_FLAG_Busy) != RESET); 45 /* 发送自动刷新命令*/ 46 FMC_SDRAMCmdConfig(&FMC_SDRAMCommandStructure); 47 48 /* Step 7 ----------------------------------------------*/ 49 /* 设置 sdram 寄存器配置 */ 50 tmpr = (uint32_t)SDRAM_MODEREG_BURST_LENGTH_8 | 51 SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL | 52 SDRAM_MODEREG_CAS_LATENCY_2 | 53 SDRAM_MODEREG_OPERATING_MODE_STANDARD | 54 SDRAM_MODEREG_WRITEBURST_MODE_SINGLE; 55 56 /* 配置命令:设置 SDRAM 寄存器 */ 第 320 页 共 928 零死角玩转 STM32—F429 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 } FMC_SDRAMCommandStructure.FMC_CommandMode = FMC_Command_Mode_LoadMode; FMC_SDRAMCommandStructure.FMC_CommandTarget = FMC_COMMAND_TARGET_BANK; FMC_SDRAMCommandStructure.FMC_AutoRefreshNumber = 1; FMC_SDRAMCommandStructure.FMC_ModeRegisterDefinition = tmpr; /* 检查 SDRAM 标志,等待至 SDRAM 空闲 */ while (FMC_GetFlagStatus(FMC_BANK_SDRAM, FMC_FLAG_Busy) != RESET); /* 发送上述命令*/ FMC_SDRAMCmdConfig(&FMC_SDRAMCommandStructure); /* Step 8 --------------------------------------------*/ /* 设置刷新计数器 */ /*刷新速率 = (COUNT + 1) x SDRAM 频率时钟 COUNT =( SDRAM 刷新周期/行数) - 20*/ /* 64ms/4096=15.62us (15.62 us x FSDCLK) - 20 =1386 */ FMC_SetRefreshCount(1386); /* 发送上述命令*/ while (FMC_GetFlagStatus(FMC_BANK_SDRAM, FMC_FLAG_Busy) != RESET); SDRAM 的初始化流程实际上是发送一系列控制命令,利用命令结构体 FMC_SDRAMCommandTypeDef 及库函数 FMC_SDRAMCmdConfig 配合即可发送各种命令。 函数中按次序发送了使能 CLK 命令、预充电命令、2 个自动刷新命令以及加载模式寄存器 命令,每次发调用 FMC_SDRAMCmdConfig 发送命令后需要调用库函数 FMC_GetFlagStatus 检查 BUSY 标志位,等待上一个命令操作完毕。 其中发送加载模式寄存器命令时使用了一些自定义的宏,使用这些宏组合起来然后赋 值到命令结构体的 FMC_ModeRegisterDefinition 成员中,这些宏定义见代码清单 26-8。 代码清单 26-8 加载模式寄存器命令相关的宏 1 /** 2 * @brief FMC SDRAM 模式配置的寄存器相关定义 3 */ 4 #define SDRAM_MODEREG_BURST_LENGTH_1 ((uint16_t)0x0000) 5 #define SDRAM_MODEREG_BURST_LENGTH_2 ((uint16_t)0x0001) 6 #define SDRAM_MODEREG_BURST_LENGTH_4 ((uint16_t)0x0002) 7 #define SDRAM_MODEREG_BURST_LENGTH_8 ((uint16_t)0x0004) 8 #define SDRAM_MODEREG_BURST_TYPE_SEQUENTIAL ((uint16_t)0x0000) 9 #define SDRAM_MODEREG_BURST_TYPE_INTERLEAVED ((uint16_t)0x0008) 10 #define SDRAM_MODEREG_CAS_LATENCY_2 ((uint16_t)0x0020) 11 #define SDRAM_MODEREG_CAS_LATENCY_3 ((uint16_t)0x0030) 12 #define SDRAM_MODEREG_OPERATING_MODE_STANDARD ((uint16_t)0x0000) 13 #define SDRAM_MODEREG_WRITEBURST_MODE_PROGRAMMED ((uint16_t)0x0000) 14 #define SDRAM_MODEREG_WRITEBURST_MODE_SINGLE ((uint16_t)0x0200) 这些宏是根据“SDRAM 的模式寄存器”的位定义的,例如突发长度、突发模式、CL 长度、SDRAM 工作模式以及突发写模式,其中的 CL 长度注意要与前面 FMC SDRAN 初 始化结构体中定义的一致。 设置自动刷新周期 在上面 SDRAM_InitSequence 函数的最后,我们还调用了库函数 FMC_SetRefreshCount 设置 FMC 自动刷新周期,这个函数会向刷新定时寄存器 FMC_SDRTR 写入计数值,这个 计数值每个 SDCLK 周期自动减 1,减至 0 时 FMC 会自动向 SDRAM 发出自动刷新命令, 控制 SDRAM 刷新,SDRAM 每次收到刷新命令后,刷新一行,对同一行进行刷新操作的 时间间隔称为 SDRAM 的刷新周期。 根据 STM32F4xx 参考手册的说明,COUNT 值的计算公式如下: 第 321 页 共 928 零死角玩转 STM32—F429 刷新速率 = (COUNT + 1) x SDRAM 频率时钟 COUNT =( SDRAM 刷新周期/行数) – 20 而查询我们的 SDRAM 芯片规格书,可知它的 SDRAM 刷新周期为 64ms,行数为 4096,可算出它的 SDRAM 刷新要求: TRefresh = 64ms/4096=15.62us 即每隔 15.62us 需要收到一次自动刷新命令。 所以: COUNTA = TRefresh/TSDCLK=15.62x90=1406 但是根据要求,如果 SDRAM 在接受读请求后出现内部刷新请求,则必须将刷新速率 增加 20 个 SDRAM 时钟周期以获得重充足的裕量。 最后计算出:COUNT=COUNTA-20=1386。 以上就是函数 FMC_SetRefreshCount 参数值的计算过程。 使用指针的方式访问 SDRAM 存储器 完成初始化 SDRAM 后,我们就可以利用它存储数据了,由于 SDRAM 的存储空间是 被映射到内核的寻址区域的,我们可以通过映射的地址直接访问 SDRAM,访问这些地址 时,FMC 外设自动读写 SDRAM,程序上无需额外操作。 通过地址访问内存,最直接的方式就是使用 C 语言的指针方式了,见代码清单 26-9。 代码清单 26-9 使用指针的方式访问 SDRAM 1 /*SDRAM 起始地址 存储空间 2 的起始地址*/ 2 #define SDRAM_BANK_ADDR ((uint32_t)0xD0000000) 3 /*SDRAM 大小,8M 字节*/ 4 #define IS42S16400J_SIZE 0x800000 5 6 uint32_t temp; 7 8 /*向 SDRAM 写入 8 位数据*/ 9 *( uint8_t*) (SDRAM_BANK_ADDR ) = (uint8_t)0xAA; 10 /*从 SDRAM 读取数据*/ 11 temp = *( uint8_t*) (SDRAM_BANK_ADDR ); 12 13 /*写/读 16 位数据*/ 14 *( uint16_t*) (SDRAM_BANK_ADDR+10 ) = (uint16_t)0xBBBB; 15 temp = *( uint16_t*) (SDRAM_BANK_ADDR+10 ); 16 17 /*写/读 32 位数据*/ 18 *( uint32_t*) (SDRAM_BANK_ADDR+20 ) = (uint32_t)0xCCCCCCCC; 19 temp = *( uint32_t*) (SDRAM_BANK_ADDR+20 ); 为方便使用,代码中首先定义了宏 SDRAM_BANK_ADDR 表示 SDRAM 的起始地址, 该地址即 FMC 映射的存储区域 2 的首地址;宏 IS42S16400J_SIZE 表示 SDRAM 的大小, 所以从地址(SDRAM_BANK_ADDR)到(SDRAM_BANK_ADDR+IS42S16400J_SIZE)都表示 在 SDRAM 的存储空间,访问这些地址,直接就能访问 SDRAM。 配合这些宏,使用指针的强制转换以及取指针操作即可读写 SDRAM 的数据,使用上 跟普通的变量无异。 第 322 页 共 928 零死角玩转 STM32—F429 直接指定变量存储到 SDRAM 空间 每次存取数据都使用指针来访问太麻烦了,为了简化操作,可以直接指定变量存储到 SDRAM 空间,见代码清单 26-10。 代码清单 26-10 直接指定变量地址的方式访问 SDRAM 1 /*SDRAM 起始地址 存储空间 2 的起始地址*/ 2 #define SDRAM_BANK_ADDR ((uint32_t)0xD0000000) 3 /*绝对定位方式访问 SDRAM,这种方式必须定义成全局变量*/ 4 uint8_t testValue __attribute__((at(SDRAM_BANK_ADDR))); 5 testValue = 0xDD; 这种方式使用关键字“__attribute__((at()))”来指定变量的地址,代码中指定 testValue 存储到 SDRAM 的起始地址,从而实现把变量存储到 SDRAM 上。要注意使用这种方法定 义变量时,必须在函数外把它定义成全局变量,才可以存储到指定地址上。 更常见的是利用这种方法定义一个很大的数组,整个数组都指定到 SDRAM 地址上, 然后就像使用 malloc 函数一样,用户自定义一些内存管理函数,动态地使用 SDRAM 的内 存,我们在使用 emWin 写 GUI 应用的时候就是这样做的。 在本书的《MDK 编译过程及文件类型全解》章节将会讲解使用更简单的方法从 SDRAM 中分配变量,以及使用 C 语言标准库的 malloc 函数来分配 SDRAM 的空间,更有 效地进行内存管理。 3. main 函数 最后我们来编写 main 函数,进行 SDRAM 芯片读写校验,见代码清单 24-14。 代码清单 26-11 main 函数 1 /** 2 * @brief 主函数 3 * @param 无 4 * @retval 无 5 */ 6 int main(void) 7{ 8 /* LED 端口初始化 */ 9 LED_GPIO_Config(); 10 /* 初始化串口 */ 11 Debug_USART_Config(); 12 printf("\r\n 秉火 STM32F429 SDRAM 读写测试例程\r\n"); 13 /*初始化 SDRAM 模块*/ 14 SDRAM_Init(); 15 16 /*蓝灯亮,表示正在读写 SDRAM 测试*/ 17 LED_BLUE; 18 /*对 SDRAM 进行读写测试,检测 SDRAM 是否正常*/ 19 if (SDRAM_Test()==1) 20 { 21 //测试正常 绿灯亮 22 LED_GREEN; 23 } 24 else 25 { 26 //测试失败 红灯亮 27 LED_RED; 28 } 第 323 页 共 928 零死角玩转 STM32—F429 29 while (1); 30 } 函数中初始化了 LED、串口,接着调用前面定义好的 SDRAM_Init 函数初始化 FMC 及 SDRAM,然后调用自定义的测试函数 SDRAM_Test 尝试使用 SDRAM 存取 8、16 及 32 位数据,并进行读写校验,它就是使用指针的方式存取数据并校验而已,此处不展开。 注意对 SDRAM 存储空间的数据操作都要在 SDRAM_Init 初始化 FMC 之后,否则数据 是无法正常存储的。 下载验证 用 USB 线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。在串口调试助手可看到 SDRAM 测试的调试信息。 26.9 每课一问 5. 如果要把 SDRAM 映射到 FMC SDRAM 的存储区域 1,需要如何修改 STM32 与 SDRAM 的硬件连接?程序上需要修改哪些内容? 第 324 页 共 928 零死角玩转 STM32—F429 第27章 LTDC/DMA2D—液晶显示 本章参考资料:《STM32F4xx 参考手册 2》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 关于开发板配套的液晶屏参数可查阅《5.0 寸液晶屏数据手册》配套资料获知。 27.1 显示器简介 显示器属于计算机的 I/O 设备,即输入输出设备。它是一种将特定电子信息输出到屏 幕上再反射到人眼的显示工具。常见的有 CRT 显示器、液晶显示器、LED 点阵显示器及 OLED 显示器。 27.1.1 液晶显示器 液晶显示器,简称 LCD(Liquid Crystal Display),相对于上一代 CRT 显示器(阴极射线 管显示器),LCD 显示器具有功耗低、体积小、承载的信息量大及不伤眼的优点,因而它成 为了现在的主流电子显示设备,其中包括电视、电脑显示器、手机屏幕及各种嵌入式设备 的显示器。图 27-1 是液晶电视与 CRT 电视的外观对比,很明显液晶电视更薄,“时尚” 是液晶电视给人的第一印象,而 CRT 电视则感觉很“笨重”。 图 27-1 液晶电视及 CRT 电视 液晶是一种介于固体和液体之间的特殊物质,它是一种有机化合物,常态下呈液态, 但是它的分子排列却和固体晶体一样非常规则,因此取名液晶。如果给液晶施加电场,会 改变它的分子排列,从而改变光线的传播方向,配合偏振光片,它就具有控制光线透过率 的作用,再配合彩色滤光片,改变加给液晶电压大小,就能改变某一颜色透光量的多少, 图 27-2 中的就是绿色显示结构。利用这种原理,做出可控红、绿、蓝光输出强度的显示结 构,把三种显示结构组成一个显示单位,通过控制红绿蓝的强度,可以使该单位混合输出 不同的色彩,这样的一个显示单位被称为像素。 第 325 页 共 928 零死角玩转 STM32—F429 图 27-2 液晶屏的绿色显示结构 注意液晶本身是不发光的,所以需要有一个背光灯提供光源,光线经过一系列处理过 程才到输出,所以输出的光线强度是要比光源的强度低很多的,比较浪费能源(当然,比 CRT 显示器还是节能多了)。而且这些处理过程会导致显示方向比较窄,也就是它的视角较 小,从侧面看屏幕会看不清它的显示内容。另外,输出的色彩变换时,液晶分子转动也需 要消耗一定的时间,导致屏幕的响应速度低。 27.1.2 LED 和 OLED 显示器 LED 点阵显示器不存在以上液晶显示器的问题,LED 点阵彩色显示器的单个像素点内 包含红绿蓝三色 LED 灯,显示原理类似我们实验板上的 LED 彩灯,通过控制红绿蓝颜色 的强度进行混色,实现全彩颜色输出,多个像素点构成一个屏幕。由于每个像素点都是 LED 灯自发光的,所以在户外白天也显示得非常清晰,但由于 LED 灯体积较大,导致屏幕 的像素密度低,所以它一般只适合用于广场上的巨型显示器。相对来说,单色的 LED 点阵 显示器应用得更广泛,如公交车上的信息展示牌、店招等,见图 27-3。 图 27-3 LED 点阵彩屏有 LED 单色显示屏 新一代的 OLED 显示器与 LED 点阵彩色显示器的原理类似,但由于它采用的像素单元 是“有机发光二极管”(Organic Light Emitting Diode),所以像素密度比普通 LED 点阵显示 器高得多,见图 27-5。 第 326 页 共 928 零死角玩转 STM32—F429 图 27-4 OLED 像素结构 OLED 显示器不需要背光源、对比度高、轻薄、视角广及响应速度快等优点。待到生 产工艺更加成熟时,必将取代现在液晶显示器的地位,见图 27-5。 图 27-5 采用 OLED 屏幕的电视及智能手表 27.1.3 显示器的基本参数 不管是哪一种显示器,都有一定的参数用于描述它们的特性,各个参数介绍如下: (1) 像素 像素是组成图像的最基本单元要素,显示器的像素指它成像最小的点,即前面讲解液 晶原理中提到的一个显示单元。 (1) 分辨率 一些嵌入式设备的显示器常常以“行像素值 x 列像素值”表示屏幕的分辨率。如分辨 率 800x480 表示该显示器的每一行有 800 个像素点,每一列有 480 个像素点,也可理 解为有 800 列,480 行。 (2) 色彩深度 色彩深度指显示器的每个像素点能表示多少种颜色,一般用“位”(bit)来表示。如单 色屏的每个像素点能表示亮或灭两种状态(即实际上能显示 2 种颜色),用 1 个数据位 第 327 页 共 928 零死角玩转 STM32—F429 就可以表示像素点的所有状态,所以它的色彩深度为 1bit,其它常见的显示屏色深为 16bit、24bit。 (3) 显示器尺寸 显示器的大小一般以英寸表示,如 5 英寸、21 英寸、24 英寸等,这个长度是指屏幕对 角线的长度, 通过显示器的对角线长度及长宽比可确定显示器的实际长宽尺寸。 (4) 点距 点距指两个相邻像素点之间的距离,它会影响画质的细腻度及观看距离,相同尺寸的 屏幕,若分辨率越高,则点距越小,画质越细腻。如现在有些手机的屏幕分辨率比电 脑显示器的还大,这是手机屏幕点距小的原因;LED 点阵显示屏的点距一般都比较大, 所以适合远距离观看。 27.2 液晶控制原理 图 27-6 是两种适合于 STM32 芯片使用的显示屏,我们以它为例讲解控制液晶屏的原 理。 第 328 页 共 928 零死角玩转 STM32—F429 图 27-6 适合 STM32 控制的显示屏实物图 这个完整的显示屏由液晶显示面板、电容触摸面板以及 PCB 底板构成。图中的触摸面 板带有触摸控制芯片,该芯片处理触摸信号并通过引出的信号线与外部器件通讯面板中间 是透明的,它贴在液晶面板上面,一起构成屏幕的主体,触摸面板与液晶面板引出的排线 连接到 PCB 底板上,根据实际需要,PCB 底板上可能会带有“液晶控制器芯片”。因为控 制液晶面板需要比较多的资源,所以大部分低级微控制器都不能直接控制液晶面板,需要 额外配套一个专用液晶控制器来处理显示过程,外部微控制器只要把它希望显示的数据直 接交给液晶控制器即可。而不带液晶控制器的 PCB 底板 ,只有小部分的电源管理电路, 液晶面板的信号线与外部微控制器相连,直接控制。STM32F429 系列的芯片不需要额外的 液晶控制器,也就是说它把专用液晶控制器的功能集成到 STM32F429 芯片内部了,节约 了额外的控制器成本。 第 329 页 共 928 零死角玩转 STM32—F429 27.2.1 液晶面板的控制信号 本章我们主要讲解控制液晶面板,液晶面板的控制信号线见表 27-1。 表 27-1 液晶面板的信号线 信号名称 说明 (1) RGB 信号线 R[7:0] G[7:0] B[7:0] CLK HSYNC VSYNC DE 红色数据 绿色数据 蓝色数据 像素同步时钟信号 水平同步信号 垂直同步信号 数据使能信号 RGB 信号线各有 8 根,分别用于表示液晶屏一个像素点的红、绿、蓝颜色分量。使用 红绿蓝颜色分量来表示颜色是一种通用的做法,打开 Windows 系统自带的画板调色工 具,可看到颜色的红绿蓝分量值,见图 27-7。常见的颜色表示会在“RGB”后面附带 各个颜色分量值的数据位数,如 RGB565 表示红绿蓝的数据线数分别为 5、6、5 根, 一共为 16 个数据位,可表示 216 种颜色;而这个液晶屏的种颜色分量的数据线都有 8 根,所以它支持 RGB888 格式,一共 24 位数据线,可表示的颜色为 224 种。 图 27-7 颜色表示法 (2) 同步时钟信号 CLK 液晶屏与外部使用同步通讯方式,以 CLK 信号作为同步时钟,在同步时钟的驱动下, 每个时钟传输一个像素点数据。 (3) 水平同步信号 HSYNC 水平同步信号 HSYNC(Horizontal Sync)用于表示液晶屏一行像素数据的传输结束,每 传输完成液晶屏的一行像素数据时,HSYNC 会发生电平跳变,如分辨率为 800x480 的 显示屏(800 列,480 行),传输一帧的图像 HSYNC 的电平会跳变 480 次。 (4) 垂直同步信号 VSYNC 垂直同步信号 VSYNC(Vertical Sync)用于表示液晶屏一帧像素数据的传输结束,每传 输完成一帧像素数据时,VSYNC 会发生电平跳变。其中“帧”是图像的单位,一幅 图像称为一帧,在液晶屏中,一帧指一个完整屏液晶像素点。人们常常用“帧/秒”来 第 330 页 共 928 零死角玩转 STM32—F429 表示液晶屏的刷新特性,即液晶屏每秒可以显示多少帧图像,如液晶屏以 60 帧/秒的 速率运行时,VSYNC 每秒钟电平会跳变 60 次。 (5) 数据使能信号 DE 数据使能信号 DE(Data Enable)用于表示数据的有效性,当 DE 信号线为高电平时, RGB 信号线表示的数据有效。 27.2.2 液晶数据传输时序 通过上述信号线向液晶屏传输像素数据时,各信号线的时序见图 27-8。图中表示的是 向液晶屏传输一帧图像数据的时序,中间省略了多行及多个像素点。 图 27-8 液晶时序图 液晶屏显示的图像可看作一个矩形,结合图 27-9 来理解。液晶屏有一个显示指针,它 指向将要显示的像素。显示指针的扫描方向方向从左到右、从上到下,一个像素点一个像 素点地描绘图形。这些像素点的数据通过 RGB 数据线传输至液晶屏,它们在同步时钟 CLK 的驱动下一个一个地传输到液晶屏中,交给显示指针,传输完成一行时,水平同步信 号 HSYNC 电平跳变一次,而传输完一帧时 VSYNC 电平跳变一次。 第 331 页 共 928 零死角玩转 STM32—F429 图 27-9 液晶数据传输图解 但是,液晶显示指针在行与行之间,帧与帧之间切换时需要延时,而且 HSYNC 及 VSYNC 信号本身也有宽度,这些时间参数说明见表 27-2。 表 27-2 液晶通讯中的时间参数 时间参数 参数说明 VBP (vertical back porch) 表示在一帧图像开始时,垂直同步信号以后的无效的行数 VFP (vertical front porch) 表示在一帧图像结束后,垂直同步信号以前的无效的行数 HBP (horizontal back porch) 表示从水平同步信号开始到一行的有效数据开始之间的 CLK 的个数 HFP (horizontal front porth) 表示一行的有效数据结束到下一个水平同步信号开始之间的 VSW (vertical sync width) CLK 的个数 表示垂直同步信号的宽度,单位为行 HSW (horizontal sync width) 表示水平同步信号的宽度,单位为同步时钟 CLK 的个数 在这些时间参数控制的区域,数据使能信号线“DE”都为低电平,RGB 数据线的信 号无效,当“DE”为高电平时,表示的数据有效,传输的数据会直接影响液晶屏的显示区 域。 27.2.3 显存 液晶屏中的每个像素点都是数据,在实际应用中需要把每个像素点的数据缓存起来, 再传输给液晶屏,这种存储显示数据的存储器被称为显存。显存一般至少要能存储液晶屏 的一帧显示数据,如分辨率为 800x480 的液晶屏,使用 RGB888 格式显示,它的一帧显示 数据大小为:3x800x480=1152000 字节;若使用 RGB565 格式显示,一帧显示数据大小为: 2x800x480=768000 字节。 第 332 页 共 928 零死角玩转 STM32—F429 27.3 LTDC 液晶控制器简介 STM32F429 系列芯片内部自带一个 LTDC 液晶控制器,使用 SDRAM 的部分空间作为 显存,可直接控制液晶面板,无需额外增加液晶控制器芯片。STM32 的 LTDC 液晶控制器 最高支持 800x600 分辨率的屏幕;可支持多种颜色格式,包括 RGB888、RGB565、 ARGB8888 和 ARGB1555 等(其中的“A”是指透明像素);支持 2 层显示数据混合,利用这 个特性,可高效地做出背景和前景分离的显示效果,如以视频为背景,在前景显示弹幕。 27.3.1 图像数据混合 LTDC 外设支持 2 层数据混合,混合前使用 2 层数据源,分别为前景层和背景层,见 图 27-10。在输出时,实际上液晶屏只能显示一层图像,所以 LTDC 在输出数据到液晶屏 前需要把 2 层图像混合成一层,跟 Photoshop 软件的分层合成图片过程类似。混合时,直 接用前景层中的不透明像素替换相同位置的背景像素;而前景层中透明像素的位置,则使 用背景的像素数据,即显示背景层的像素。 图 27-10 图像的分层与混合 如果想使用图像混合功能,前景层必须使用包含透明的像素格式,如 ARGB1555 或 ARGB8888。其中 ARGB1555 使用 1 个数据位表示透明元素,它只能表示像素是透明或不 透明,当最高位(即“A”位)为 1 时,表示这是一个不透明的像素,具体颜色值为 RGB 位 表示的颜色,而当最高位为 0 时,表示这是一个完全透明的像素,RGB 位的数据无效;而 ARGB8888 的像素格式使用 8 个数据位表示透明元素,它使用高 8 位表示“透明度”(即代 表“A”的 8 个数据位),若 A 的值为“0xFF”,则表示这个像素完全不透明,若 A 的值为 “0x00”则表示这个像素完全透明,介于它们之间的值表示其 RGB 颜色不同程度的透明度, 即混合后背景像素根据这个值按比例来表示。 第 333 页 共 928 零死角玩转 STM32—F429 注意液晶屏本身是没有透明度概念的,如 24 位液晶屏的像素数据格式是 RGB888, RGB 颜色各有对应的 8 根数据线,不存在用于表示透明度的数据线,所以实际上 ARGB 只 是针对内部分层数据处理的格式,最终经过混合运算得出直接颜色数据 RGB888 才能交给 液晶屏显示。 27.3.2 LTDC 结构框图剖析 图 27-11 是 LTDC 控制器的结构框图,它主要包含信号线、图像处理单元、寄存器及 时钟信号。 图 27-11 LTDC 控制器框图 1. LTDC 信号线 LTDC 的控制信号线与液晶显示面板的数据线一一对应,包含有 HSYNC、VSYNC、 DE、CLK 及 RGB 数据线各 8 根。设计硬件时把液晶面板与 STM32 对应的这些引脚连接起 来即可,查阅《STM32F4xx 规格书》可知 LTDC 信号线对应的引脚,见表 27-3。 表 27-3 LTDC 引脚表 引脚号 PA3 PA4 PA6 PA8 PA11 PA12 PB8 PB9 PB10 PB11 LTDC 信号 LCD_B5 LCD_VSYNC LCD_G2 LCD_R6 LCD_R4 LCD_R5 LCD_B6 LCD_B7 LCD_G4 LCDG5 引脚号 PE11 PE12 PE13 PE14 PE15 PF10 PG6 PG7 PG10 PG11 LTDC 信号 LCD_G3 LCD_B4 LCD_DE LCD_CLK LCD_R7 LCD_DE LCD_R7 LCD_CLK LCD_B2 LCD_B3 引脚号 PH14 PH15 PI0 PI1 PI2 PI4 PI5 PI6 PI7 PI9 LTDC 信号 LCD_G3 LCD_G4 LCD_G5 LCD_G6 LCD_G7 LCD_B4 LCD_B5 LCD_B6 LCD_B7 LCD_VSYNC 引脚号 PJ4 PJ5 PJ6 PJ7 PJ8 PJ9 PJ10 PJ11 PJ12 PJ13 LTDC 信号 LCD_R5 LCD_R6 LCD_R7 LCD_G0 LCD_G1 LCD_G2 LCD_G3 LCD_G4 LCD_B0 LCD_B1 第 334 页 共 928 零死角玩转 STM32—F429 PC6 PC7 PC10 PD3 PD6 PD10 PE4 PE5 PE6 LCD_HSYNC LCD_G6 LCD_R2 LCD_G7 LCD_B2 LCD_B3 LCD_B0 LCD_G0 LCD_G1 PG12 PH2 PH3 PH8 PH9 PH10 PH11 PH12 PH13 LCD_B1 LCD_R0 LCD_R1 LCD_R2 LCD_R3 LCD_R4 LCD_R5 LCD_R6 LCD_G2 PI10 PI12 PI13 PI14 PI15 PJ0 PJ1 PJ2 PJ3 LCD_HSYNC LCD_HSYNC LCD_VSYNC LCD_CLK LCD_R0 LCD_R1 LCD_R2 LCD_R3 LCD_R4 PJ14 PJ15 PK0 PK1 PK2 PK3 PK4 PK5 PK6 LCD_B2 LCD_B3 LCD_G5 LCD_G6 LCD_G7 LCD_B4 LCD_B5 LCD_B6 LCD_B7 2. 图像处理单元 LTDC 框图标号表示的是图像处理单元,它通过“AHB 接口”获取显存中的数据, 然后按分层把数据分别发送到两个“层 FIFO”缓存,每个 FIFO 可缓存 64x32 位的数据, 接着从缓存中获取数据交给“PFC”(像素格式转换器),它把数据从像素格式转换成字 (ARGB8888)的格式,再经过“混合单元”把两层数据合并起来,最终混合得到的是单层要 显示的数据,通过信号线输出到液晶面板。这部分结构与 DMA2D 的很类似,我们在下一 小节详细讲解。 在输出前混合单元的数据还经过一个“抖动单元”,它的作用是当像素数据格式的色 深大于液晶面板实际色深时,对像素数据颜色进行舍入操作,如向 18 位显示器上显示 24 位数据时,抖动单元把像素数据的低 6 位与阈值比较,若大于阈值,则向数据的第 7 位进 1,否则直接舍掉低 6 位。 3. 配置和状态寄存器 框图中标号表示的是 LTDC 的控制逻辑,它包含了 LTDC 的各种配置和状态寄存器。 如配置与液晶面板通讯时信号线的有效电平、各种时间参数、有效数据宽度、像素格式及 显存址等等,LTDC 外设根据这些配置控制数据输出,使用 AHB 接口从显存地址中搬运数 据到液晶面板。还有一系列用于指示当前显示状态和位置的状态寄存器,通过读取这些寄 存器可以了解 LTDC 的工作状态。 4. 时钟信号 LTDC 外设使用 3 种时钟信号,包括 AHB 时钟、APB2 时钟及像素时钟 LCD_CLK。 AHB 时钟用于驱动数据从存储器存储到 FIFO,APB2 时钟用于驱动 LTDC 的寄存器。而 LCD_CLK 用于生成与液晶面板通讯的同步时钟,见图 27-12,它的来源是 HSE(高速外部 晶振),经过“/M”分频因子分频输出到“PLLSAI”分频器,信号由“PLLSAI”中的倍频 因子 N 倍频得到“PLLSAIN”时钟、然后由“/R”因子分频得到“PLLCDCLK”时钟,再 经过“DIV”因子得到“LCD-TFT clock”,“LCD-TFT clock”即通讯中的同步时钟 LCD_CLK,它使用 LCD_CLK 引脚输出。 第 335 页 共 928 零死角玩转 STM32—F429 图 27-12 LCD_CLK 时钟来源 27.4 DMA2D 图形加速器简介 在实际使用 LTDC 控制器控制液晶屏时,使 LTDC 正常工作后,往配置好的显存地址 写入要显示的像素数据,LTDC 就会把这些数据从显存搬运到液晶面板进行显示,而显示 数据的容量非常大,所以我们希望能用 DMA 来操作,针对这个需求,STM32 专门定制了 DMA2D 外设,它可用于快速绘制矩形、直线、分层数据混合、数据复制以及进行图像数 据格式转换,可以把它理解为图形专用的 DMA。 27.4.1 DMA2D 结构框图剖析 图 27-13 是 DMA2D 的结构框图,它与前面 LTDC 结构里的图像处理单元很类似,主 要为分层 FIFO、PFC 及彩色混合器。 第 336 页 共 928 零死角玩转 STM32—F429 图 27-13 DMA2D 结构框图 1. FG FIFO 与 BG FIFO FG FIFO(Foreground FIFO)与 BG FIFO(Backgroun FIFO)是两个 64x32 位大小的缓冲区, 它们用于缓存从 AHB 总线获取的像素数据,分别专用于缓冲前景层和背景层的数据源。 AHB 总线的数据源一般是 SDRAM,也就是说在 LTDC 外设中配置的前景层及背景层 数据源地址一般指向 SDRAM 的存储空间,使用 SDRAM 的部分空间作为显存。 2. FG PFC 与 BG PFC FG PFC(FG Pixel Format Convertor)与 BG PFC(BG Pixel Format Convertor)是两个像素格 式转换器,分别用于前景层和背景层的像素格式转换,不管从 FIFO 的数据源格式如何, 都把它转化成字的格式(即 32 位),ARGB8888。 图中的“ɑ”表示 Alpha,即透明度,经过 PFC,透明度会被扩展成 8 位的格式。 图中的“CLUT”表示颜色查找表(Color Lookup Table),颜色查找表是一种间接的颜色 表示方式,它使用一个 256x32 位的空间缓存 256 种颜色,颜色的格式是 ARGB8888 或 RGB888。见图 27-14,利用颜色查找表,实际的图像只使用这 256 种颜色,而图像的每个 像素使用 8 位的数据来表示,该数据并不是直接的 RGB 颜色数据,而是指向颜色查找表的 地址偏移,即表示这个像素点应该显示颜色查找表中的哪一种颜色。在图像大小不变的情 况下,利用颜色查找表可以扩展颜色显示的能力,其特点是用 8 位的数据表示了一个 24 或 第 337 页 共 928 零死角玩转 STM32—F429 32 位的颜色,但整个图像颜色的种类局限于颜色表中的 256 种。DMA2D 的颜色查找表可 以由 CPU 自动加载或编程手动加载。 图 27-14 使用颜色查找表显示图像的过程 3. 混合器 FIFO 中的数据源经过 PFC 像素格式转换器后,前景层和背景层的图像都输入到混合 器中运算,运算公式见图 27-15。 图 27-15 混合公式 从公式可以了解到混合器的运算主要是使用前景和背景的透明度作为因子,对像素 RGB 颜色值进行加权运算。经过混合器后,两层数据合成为一层 ARGB8888 格式的图像。 4. OUT PFC OUT PFC 是输出像素格式转换器,它把混合器转换得到的图像转换成目标格式,如 ARGB8888、RGB888、RGB565、ARGB1555 或 ARGB4444,具体的格式可根据需要在输 出 PFC 控制寄存器 DMA2D_OPFCCR 中选择。 第 338 页 共 928 零死角玩转 STM32—F429 STM32F429 芯片使用 LTDC、DMA2D 及 RAM 存储器,构成了一个完整的液晶控制 器。LTDC 负责不断刷新液晶屏,DMA2D 用于图像数据搬运、混合及格式转换,RAM 存 储器作为显存。其中显存可以使用 STM32 芯片内部的 SRAM 或外扩 SDRAM/SRAM,只 要容量足够大即可(至少要能存储一帧图像数据)。 27.5 LTDC 初始化结构体 控制 LTDC 涉及到非常多的寄存器,利用 LTDC 初始化结构体可以减轻开发和维护的 工作量,LTDC 初始化结构体见代码清单 24-1。 代码清单 27-1 LTDC 初始化结构体 LTDC_InitTypeDef 1 /** 2 * @brief LTDC Init structure definition 3 */ 4 typedef struct 5{ 6 uint32_t LTDC_HSPolarity; /*配置行同步信号 HSYNC 的极性 */ 7 uint32_t LTDC_VSPolarity; /*配置垂直同步信号 VSYNC 的极性 */ 8 uint32_t LTDC_DEPolarity; /*配置数据使能信号 DE 的极性*/ 9 uint32_t LTDC_PCPolarity; /*配置像素时钟信号 CLK 的极性 */ 10 uint32_t LTDC_HorizontalSync; /*配置行同步信号 HSYNC 的宽度(HSW-1) */ 11 uint32_t LTDC_VerticalSync; /*配置垂直同步信号 VSYNC 的宽度(VSW-1) */ 12 uint32_t LTDC_AccumulatedHBP; /*配置(HSW+HBP-1)的值*/ 13 uint32_t LTDC_AccumulatedVBP; /*配置(VSW+VBP-1)的值*/ 14 uint32_t LTDC_AccumulatedActiveW; /*配置(HSW+HBP+有效宽度-1)的值*/ 15 uint32_t LTDC_AccumulatedActiveH; /*配置(VSW+VBP+有效高度-1)的值*/ 16 uint32_t LTDC_TotalWidth; /*配置(HSW+HBP+有效宽度+HFP-1)的值 */ 17 uint32_t LTDC_TotalHeigh; /*配置(VSW+VBP+有效高度+VFP-1)的值 */ 18 uint32_t LTDC_BackgroundRedValue; /*配置背景的红色值*/ 19 uint32_t LTDC_BackgroundGreenValue; /*配置背景的绿色值*/ 20 uint32_t LTDC_BackgroundBlueValue; /*配置背景的蓝色值*/ 21 } LTDC_InitTypeDef; 这个结构体大部分成员都是用于定义 LTDC 的时序参数的,包括信号有效电平及各种 时间参数的宽度,配合“液晶数据传输时序”中的说明更易理解。各个成员介绍如下,括 号中的是 STM32 标准库定义的宏: (8) LTDC_HSPolarity 本成员用于设置行同步信号 HSYNC 的极性,即 HSYNC 有效时的电平,该成员的值 可设置为高电平(LTDC_HSPolarity_AH)或低电平(LTDC_HSPolarity_AL)。 (9) LTDC_VSPolarity 本成员用于设置垂直同步信号 VSYNC 的极性,可设置为高电平 (LTDC_VSPolarity_AH)或低电平(LTDC_VSPolarity_AL)。 (10) LTDC_DEPolarity 本成员用于设置数据使能信号 DE 的极性,可设置为高电平(LTDC_DEPolarity_AH)或 低电平(LTDC_DEPolarity_AL)。 (11) LTDC_PCPolarity 第 339 页 共 928 零死角玩转 STM32—F429 本成员用于设置像素时钟信号 CLK 的极性,可设置为上升沿(LTDC_DEPolarity_AH) 或下降沿(LTDC_DEPolarity_AL),表示 RGB 数据信号在 CLK 的哪个时刻被采集。 (12) LTDC_HorizontalSync 本成员设置行同步信号 HSYNC 的宽度 HSW,它以像素时钟 CLK 的周期为单位,实 际写入该参数时应写入(HSW-1),参数范围为 0x000- 0xFFF。 (13) LTDC_VerticalSync 本成员设置垂直同步信号 VSYNC 的宽度 VSW,它以“行”为位,实际写入该参数时 应写入(VSW-1) ,参数范围为 0x000- 0x7FF。 (14) LTDC_AccumulatedHBP 本成员用于配置“水平同步像素 HSW”加“水平后沿像素 HBP”的累加值,实际写 入该参数时应写入(HSW+HBP-1) ,参数范围为 0x000- 0xFFF。 (15) LTDC_AccumulatedVBP 本成员用于配置“垂直同步行 VSW”加“垂直后沿行 VBP”的累加值,实际写入该参 数时应写入(VSW+VBP-1) ,参数范围为 0x000- 0x7FF。 (16) LTDC_AccumulatedActiveW 本成员用于配置“水平同步像素 HSW”加“水平后沿像素 HBP”加“有效像素”的累 加值,实际写入该参数时应写入(HSW+HBP+有效宽度-1) ,参数范围为 0x000- 0xFFF。 (17) LTDC_AccumulatedActiveH 本成员用于配置“垂直同步行 VSW”加“垂直后沿行 VBP”加“有效行”的累加值,实 际写入该参数时应写入(VSW+VBP+有效高度-1) ,参数范围为 0x000- 0x7FF。 (18) LTDC_TotalWidth 本成员用于配置“水平同步像素 HSW”加“水平后沿像素 HBP”加“有效像素”加 “水平前沿像素 HFP”的累加值,即总宽度,实际写入该参数时应写入(HSW+HBP+有 效宽度+HFP-1) ,参数范围为 0x000- 0xFFF。 (19) LTDC_TotalHeigh 本成员用于配置“垂直同步行 VSW”加“垂直后沿行 VBP”加“有效行”加“垂 直前沿行 VFP”的累加值,即总高度,实际写入该参数时应写入(HSW+HBP+有效高度 +VFP-1) ,参数范围为 0x000- 0x7FF。 (20) LTDC_BackgroundRedValue/ GreenValue/ BlueValue 这三个结构体成员用于配置背景的颜色值,见图 27-16,这里说的背景层与前面提到 的“前景层/背景层”概念有点区别,它们对应下图中的“第 2 层/第 1 层”,而在这两 层之外,还有一个最终的背景层,当第 1 第 2 层都透明时,这个背景层就会被显示, 而这个背景层是一个纯色的矩形,它的颜色值就是由这三个结构体成员配置的,各成 员的参数范围为 0x00- 0xFF。 第 340 页 共 928 零死角玩转 STM32—F429 图 27-16 两层与背景混合 对这些 LTDC 初始化结构体成员赋值后,调用库函数 LTDC_Init 可把这些参数写入到 LTDC 的各个配置寄存器,LTDC 外设根据这些配置控制时序。 27.6 LTDC 层级初始化结构体 LTDC 初始化结构体只是配置好了与液晶屏通讯的基本时序,还有像素格式、显存地 址等诸多参数需要使用 LTDC 层级初始化结构体完成,见代码清单 27-2。 代码清单 27-2 LTDC 层级初始化结构体 LTDC_Layer_InitTypeDef 1 /** 2 * @brief LTDC Layer structure definition 3 */ 4 typedef struct 5{ 6 uint32_t LTDC_HorizontalStart; /*配置窗口的行起始位置 */ 7 uint32_t LTDC_HorizontalStop; /*配置窗口的行结束位置 */ 8 uint32_t LTDC_VerticalStart; /*配置窗口的垂直起始位置 */ 9 uint32_t LTDC_VerticalStop; /*配置窗口的垂直束位置 */ 10 uint32_t LTDC_PixelFormat; /*配置当前层的像素格式*/ 11 uint32_t LTDC_ConstantAlpha; /*配置当前层的透明度 Alpha 常量 值*/ 12 uint32_t LTDC_DefaultColorBlue; /*配置当前层的默认蓝色值 */ 13 uint32_t LTDC_DefaultColorGreen; /*配置当前层的默认绿色值 */ 14 uint32_t LTDC_DefaultColorRed; /*配置当前层的默认红色值 */ 15 uint32_t LTDC_DefaultColorAlpha; /*配置当前层的默认透明值 */ 16 uint32_t LTDC_BlendingFactor_1; /*配置混合因子 BlendingFactor1 */ 17 uint32_t LTDC_BlendingFactor_2; /*配置混合因子 BlendingFactor2 */ 18 uint32_t LTDC_CFBStartAdress; /*配置当前层的显存起始位置*/ 19 uint32_t LTDC_CFBLineLength; /*配置当前层的行数据长度 */ 20 uint32_t LTDC_CFBPitch; /*配置从某行的起始到下一行像素起始处的增 量*/ 21 uint32_t LTDC_CFBLineNumber; /* 配置当前层的行数*/ 22 } LTDC_Layer_InitTypeDef; LTDC_Layer_InitTypeDef 各个结构体成员的功能介绍如下: (12) LTDC_ HorizontalStart /HorizontalStop/ VerticalStart/ VerticalStop 这些成员用于确定该层显示窗口的边界,分别表示行起始、行结束、垂直起始及垂直 结束的位置,见图 27-17。注意这些参数包含同步 HSW/VSW、后沿大小 HBP/VBP 和有 效数据区域的内部时序发生器的配置,表 27-4 的是各个窗口配置成员应写入的数值。 第 341 页 共 928 零死角玩转 STM32—F429 图 27-17 配置可层的显示窗口 表 27-4 各个窗口成员值 LTDC 层级窗口配置 等效于 LTDC 时序参数 实际值 成员 配置成员的值 LTDC_HorizontalStart LTDC_HorizontalStop LTDC_VerticalStart LTDC_VerticalStop (LTDC_AccumulatedHBP+1) LTDC_AccumulatedActiveW (LTDC_AccumulatedVBP+1) LTDC_AccumulatedActiveH HBP + HSW HSW+HBP+LCD_PIXEL_WIDTH1 VBP + VSW VSW+VBP+LCD_PIXEL_HEIGHT1 (13) LTDC_PixelFormat 本成员用于设置该层数据的像素格式,可以设置为 LTDC_Pixelformat_ARGB8888/ RGB888/ RGB565/ ARGB1555/ ARGB4444/ L8/ AL44/ AL88 格式。 (14) LTDC_ConstantAlpha 本成员用于设置该层恒定的透明度常量 Alpha,称为恒定 Alpha,参数范围为 0x000xFF,在图层混合时,可根据后面的 BlendingFactor 成员的配置,选择是只使用这个 恒定 Alpha 进行混合运算还是把像素本身的 Alpha 值也加入到运算中。 (15) LTDC_DefaultColorBlue/Green/Red/Alpha 这些成员用于配置该层的默认颜色值,分别为蓝、绿、红及透明分量,该颜色在定义 的层窗口外或在层禁止时使用。 (16) LTDC_BlendingFactor_1/2 本成员用于设置混合系数 BF1 和 BF2。每一层实际显示的颜色都需要使用透明度参与运 算,计算出不包含透明度的直接 RGB 颜色值,然后才传输给液晶屏(因为液晶屏本身没有 透明的概念)。混合的计算公式为: BC = BF1 x C + BF2 x Cs, 公式中的参数见表 27-5: 表 27-5 混合公式参数说明表 参数 说明 CA PAxCA BC 混 合 后 的 颜 色 ( 混 合 结 - - 果) C 当前层颜色 - - Cs 底层混合后的颜色 - - BF1 混合系数 1 等 于 ( 恒 定 Alpha 等于(恒定 Alpha x 像素 Alpha 值) 第 342 页 共 928 零死角玩转 STM32—F429 值) BF2 混合系数 2 等于(1-恒定 Alpha) 等于(1-恒定 Alpha x 像素 Alpha 值) 本结构体成员可以设置 BF1/BF2 参数使用 CA 配置(LTDC_BlendingFactor1/2_CA)还是 PAxCA 配置(LTDC_BlendingFactor1/2_PAxCA)。配置成 CA 表示混合系数中只包含恒定 的 Alpha 值,即像素本身的 Alpha 不会影响混合效果,若配置成 PAxCA,则混合系数中 包含有像素本身的 Alpha 值,即把像素本身的 Alpha 加入到混合运算中。其中的恒定 Alpha 值即前面“LTDC_ConstantAlpha”结构体配置参数的透明度百分比:(配置的 Alpha 值/0xFF)。 图 27-18 两层与背景混合 见图 27-6,数据源混合时,由下至上,如果使用了 2 层,则先将第 1 层与 LTDC 背景 混合,随后再使用该混合颜色与第 2 层混合得到最终结果。例如,当只使用第 1 层数 据源时,且 BF1 及 BF2 都配置为使用恒定 Alpha,该 Alpha 值在 LTDC_ConstantAlpha 结构体成员值中被配置为 240(0xF0)。因此,恒定 Alpha 值为 240/255=0.94。若当前层 颜色 C=128,背景色 Cs=48,那么第 1 层与背景色的混合结果为: BC=恒定 Alpha x C + (1- 恒定 Alpha) x Cs=0.94 x Cs +(1-0.94)x 48=123 (17) LTDC_CFBStartAdress 本成员用于设置该层的显存首地址,该层的像素数据保存在从这个地址开始的存储空 间内。 (18) LTDC_CFBLineLength 本成员用于设置当前层的行数据长度,即每行的有效像素点个数 x 每个像素的字节数, 实际配置该参数时应写入值(行有效像素个数 x 每个像素的字节数+3),每个像素的字 节数跟像素格式有关,如 RGB565 为 2 字节,RGB888 为 3 字节,ARGB8888 为 4 字 节。 (19) LTDC_CFBPitch 本成员用于设置从某行的有效像素起始位置到下一行起始位置处的数据增量,无特殊 情况的话,它一般就直接等于行的有效像素个数 x 每个像素的字节数。 (20) LTDC_CFBLineNumber 本成员用于设置当前层的显示行数。 配置完 LTDC_Layer_InitTypeDef 层级初始化结构体后,调用库函数 LTDC_LayerInit 可把这些配置写入到 LTDC 的层级控制寄存器中,完成初始化。初始化完成后 LTDC 会不 第 343 页 共 928 零死角玩转 STM32—F429 断把显存空间的数据传输到液晶屏进行显示,我们可以直接修改或使用 DMA2D 修改显存 中的数据,从而改变显示的内容。 27.7 DMA2D 初始化结构体 在实际显示时,我们常常采用 DMA2D 描绘直线和矩形,这个时候会用到 DMA2D 结 构体,见代码清单 27-3。 代码清单 27-3 DMA2D 初始化结构体 1 /** 2 * @brief DMA2D Init structure definition 3 */ 4 typedef struct 5{ 6 uint32_t DMA2D_Mode; 7 uint32_t DMA2D_CMode; 8 uint32_t DMA2D_OutputBlue; 9 uint32_t DMA2D_OutputGreen; 10 uint32_t DMA2D_OutputRed; 11 uint32_t DMA2D_OutputAlpha; */ 12 uint32_t DMA2D_OutputMemoryAdd; 13 uint32_t DMA2D_OutputOffset; 14 uint32_t DMA2D_NumberOfLine; 15 uint32_t DMA2D_PixelPerLine; 16 } DMA2D_InitTypeDef; /*配置 DMA2D 的传输模式*/ /*配置 DMA2D 的颜色模式 */ /*配置输出图像的蓝色分量*/ /*配置输出图像的绿色分量*/ /*配置输出图像的红色分量*/ /*配置输出图像的透明度分量 /*配置显存地址*/ /*配置输出地址偏移*/ /*配置要传输多少行 */ /*配置每行有多少个像素 */ DMA2D 初始化结构体中的各个成员介绍如下: (5) DMA2D_Mode 本成员用于配置 DMA2D 的工作模式,它可以被设置为表 27-6 中的值。 表 27-6 DMA2D 的工作模式 宏 说明 DMA2D_M2M 从存储器到存储器(仅限 FG 获取数据源) DMA2D_M2M_PFC 存储器到存储器并执行 PFC(仅限 FG PFC 激活时的 FG 获 取) DMA2D_M2M_BLEND 存储器到存储器并执行混合(执行 PFC 和混合时的 FG 和 BG 获取) DMA2D_R2M 寄存器到存储器(无 FG 和 BG,仅输出阶段激活) 这几种工作模式主要区分数据的来源、是否使能 PFC 以及是否使能混合器。使用 DMA2D 时,可把数据从某个位置搬运到显存,该位置可以是 DMA2D 本身的寄存器, 也可以是设置好的 DMA2D 前景地址、背景地址(即从存储器到存储器)。若使能了 PFC,则存储器中的数据源会经过转换再传输到显存。若使能了混合器,DMA2D 会把 两个数据源中的数据混合后再输出到显存。 若使用存储器到存储器模式,需要调用库函数 DMA2D_FGConfig,使用初始化结 构体 DMA2D_FG_InitTypeDef 配置数据源的格式、地址等参数。(背景层使用函数 DMA2D_BGConfig 和结构体 DMA2D_BG_InitTypeDef) (6) DMA2D_CMode 本成员用于配置 DMA2D 的输出 PFC 颜色格式,即它将要传输给显存的格式。 (7) DMA2D_OutputBlue/ Green/ Red/ Alpha 这几个成员用于配置 DMA2D 的寄存器颜色值,若 DMA2D 工作在“寄存器到存储器” (DMA2D_R2M)模式时,这个颜色值作为数据源,被 DMA2D 复制到显存空间,即目 标空间都会被填入这一种色彩。 (8) DMA2D_OutputMemoryAdd 第 344 页 共 928 零死角玩转 STM32—F429 本成员用于配置 DMA2D 的输出 FIFO 的地址, DMA2D 的数据会被搬运到该空间, 一般把它设置为本次传输显示位置的起始地址。 (9) DMA2D_OutputOffset 本成员用于配置行偏移(以像素为单位),行偏移会被添加到各行的结尾,用于确定下 一行的起始地址。如表 27-7 中的黄色格子表示行偏移,绿色格子表示要显示的数据。 左表中显示的是一条垂直的线,且线的宽度为 1 像素,所以行偏移的值=7-1=6,即 “行偏移的值=行宽度-线的宽度”,右表中的线宽度为 2 像素,行偏移的值=7-2=5。 表 27-7 数据传输示例(绿色的为要显示的数 的为行偏移) 01234567 据,黄色 01234567 (10) DMA2D_NumberOfLine 本成员用于配置 DMA2D 一共要传输多少行数据,如表 27-7 中一共有 5 行数据。 (11) DMA2D_PixelPerLine 本成员用于配置每行有多少个像素点,如表 27-7 左侧表示每行有 1 个像素点,右侧表 示每行有 2 个像素点。 配置完这些结构体成员,调用库函数 DMA2D_Init 即可把这些参数写入到 DMA2D 的 控制寄存器中,然后再调用 DMA2D_StartTransfer 函数开启数据传输及转换。 27.8 LTDC/DMA2D—液晶显示实验 本小节讲解如何使用 LTDC 及 DMA2D 外设控制型号为“STD800480”的 5 寸液晶屏, 见图 27-19,该液晶屏的分辨率为 800x480,支持 RGB888 格式。 学习本小节内容时,请打开配套的“LTDC/DMA2D—液晶显示英文”工程配合阅读。 27.8.1 硬件设计 图 27-19 液晶屏实物图 第 345 页 共 928 零死角玩转 STM32—F429 图 27-19 液晶屏背面的 PCB 电路对应图 27-20、图 27-21、图 27-22、图 27-24 中的原 理图,分别是升压电路、触摸屏接口、液晶屏接口及排针接口。升压电路把输入的 5V 电 源升压为 20V,输出到液晶屏的背光灯中;触摸屏及液晶屏接口通过 FPC 插座把两个屏的 排线连接到 PCB 电路板上,这些 FPC 插座与信号引出到屏幕右侧的排针处,方便整个屏 幕与外部器件相连。 图 27-20 升压电路原理图 升压电路中的 BK 引脚可外接 PWM 信号,控制液晶屏的背光强度,BK 为高电平时输 出电压。 图 27-21 电容屏接口 电容触摸屏使用 I2C 通讯,它的排线接口包含了 I2C 的通讯引脚 SCL、SDA,还包含 控制触摸屏芯片复位的 RSTN 信号以及触摸中断信号 INT。 第 346 页 共 928 零死角玩转 STM32—F429 图 27-22 液晶屏接口 关于这部分液晶屏的排线接口说明见图 27-23。 图 27-23 液晶排线接口 第 347 页 共 928 零死角玩转 STM32—F429 图 27-24 排针接口 以上是我们 STM32F429 实验板使用的 5 寸屏原理图,它通过屏幕上的排针接入到实验 板的液晶排母接口,与 STM32 芯片的引脚相连,连接见图 27-25。 图 27-25 屏幕与实验板的引脚连接 由于液晶屏的部分引脚与实验板的 CAN 芯片信号引脚相同,所以使用液晶屏的时候 不能使用 CAN 通讯。 以上原理图可查阅《LCD5.0-黑白原理图》及《秉火 F429 开发板黑白原理图》文档获 知,若您使用的液晶屏或实验板不一样,请根据实际连接的引脚修改程序。 第 348 页 共 928 零死角玩转 STM32—F429 27.8.2 软件设计 为了使工程更加有条理,我们把 LCD 控制相关的代码独立分开存储,方便以后移植。 在“FMC—读写 SDRAM”工程的基础上新建“bsp_lcd.c”及“bsp_lcd.h”文件,这些文 件也可根据您的喜好命名,它们不属于 STM32 标准库的内容,是由我们自己根据应用需要 编写的。 1. 编程要点 (19) 初始化 LTDC 时钟、DMA2D 时钟、GPIO 时钟; (20) 初始化 SDRAM,以便用作显存; (21) 根据液晶屏的参数配置 LTDC 外设的通讯时序; (22) 配置 LTDC 层级控制参数,配置显存地址; (23) 初始化 DMA2D,使用 DMA2D 辅助显示; (24) 编写测试程序,控制液晶输出。 2. 代码分析 LTDC 硬件相关宏定义 我们把 LTDC 控制液晶屏硬件相关的配置都以宏的形式定义到 “bsp_lcd.h”文件中, 见代码清单 24-2。 代码清单 27-4 LTDC 硬件配置相关的宏(省略了部分数据线) 1 /*部分液晶信号线的引脚复用编号是 AF9*/ 2 #define GPIO_AF_LTDC_AF9 3 4 //红色数据线 5 #define LTDC_R0_GPIO_PORT 6 #define LTDC_R0_GPIO_CLK 7 #define LTDC_R0_GPIO_PIN 8 #define LTDC_R0_PINSOURCE 9 #define LTDC_R0_AF 10 11 #define LTDC_R3_GPIO_PORT 12 #define LTDC_R3_GPIO_CLK 13 #define LTDC_R3_GPIO_PIN 14 #define LTDC_R3_PINSOURCE 15 #define LTDC_R3_AF 16 /*此处省略 R1、R2、R4-R7*/ 17 //绿色数据线 18 #define LTDC_G0_GPIO_PORT 19 #define LTDC_G0_GPIO_CLK 20 #define LTDC_G0_GPIO_PIN 21 #define LTDC_G0_PINSOURCE 22 #define LTDC_G0_AF 23 /*此处省略 G1-G7*/ 24 //蓝色数据线 25 #define LTDC_B0_GPIO_PORT 26 #define LTDC_B0_GPIO_CLK 27 #define LTDC_B0_GPIO_PIN 28 #define LTDC_B0_PINSOURCE 29 #define LTDC_B0_AF ((uint8_t)0x09) GPIOH RCC_AHB1Periph_GPIOH GPIO_Pin_2 GPIO_PinSource2 GPIO_AF_LTDC //使用 LTDC 复用编号 GPIOB RCC_AHB1Periph_GPIOB GPIO_Pin_0 GPIO_PinSource0 GPIO_AF_LTDC_AF9 //使用 AF9 复用编号 GPIOE RCC_AHB1Periph_GPIOE GPIO_Pin_5 GPIO_PinSource5 GPIO_AF_LTDC GPIOE RCC_AHB1Periph_GPIOE GPIO_Pin_4 GPIO_PinSource4 GPIO_AF_LTDC 第 349 页 共 928 零死角玩转 STM32—F429 30 /*此处省略 B1-B7*/ 31 32 //控制信号线 33 /*像素时钟 CLK*/ 34 #define LTDC_CLK_GPIO_PORT 35 #define LTDC_CLK_GPIO_CLK 36 #define LTDC_CLK_GPIO_PIN 37 #define LTDC_CLK_PINSOURCE 38 #define LTDC_CLK_AF 39 /*水平同步信号 HSYNC*/ 40 #define LTDC_HSYNC_GPIO_PORT 41 #define LTDC_HSYNC_GPIO_CLK 42 #define LTDC_HSYNC_GPIO_PIN 43 #define LTDC_HSYNC_PINSOURCE 44 #define LTDC_HSYNC_AF 45 /*垂直同步信号 VSYNC*/ 46 #define LTDC_VSYNC_GPIO_PORT 47 #define LTDC_VSYNC_GPIO_CLK 48 #define LTDC_VSYNC_GPIO_PIN 49 #define LTDC_VSYNC_PINSOURCE 50 #define LTDC_VSYNC_AF 51 /*数据使能信号 DE*/ 52 #define LTDC_DE_GPIO_PORT 53 #define LTDC_DE_GPIO_CLK 54 #define LTDC_DE_GPIO_PIN 55 #define LTDC_DE_PINSOURCE 56 #define LTDC_DE_AF 57 /*液晶屏使能信号 DISP,高电平使能*/ 58 #define LTDC_DISP_GPIO_PORT 59 #define LTDC_DISP_GPIO_CLK 60 #define LTDC_DISP_GPIO_PIN 61 /*液晶屏背光信号,高电平使能*/ 62 #define LTDC_BL_GPIO_PORT 63 #define LTDC_BL_GPIO_CLK 64 #define LTDC_BL_GPIO_PIN GPIOG RCC_AHB1Periph_GPIOG GPIO_Pin_7 GPIO_PinSource7 GPIO_AF_LTDC GPIOI RCC_AHB1Periph_GPIOI GPIO_Pin_10 GPIO_PinSource10 GPIO_AF_LTDC GPIOI RCC_AHB1Periph_GPIOI GPIO_Pin_9 GPIO_PinSource9 GPIO_AF_LTDC GPIOF RCC_AHB1Periph_GPIOF GPIO_Pin_10 GPIO_PinSource10 GPIO_AF_LTDC GPIOD RCC_AHB1Periph_GPIOD GPIO_Pin_4 GPIOD RCC_AHB1Periph_GPIOD GPIO_Pin_7 以上代码根据硬件的连接,把与 LTDC 与液晶屏通讯使用的引脚号、引脚源以及复用 功能映射都以宏封装起来。其中部分 LTDC 信号的复用功能映射比较特殊,如用作 R3 信 号线的 PB0,它的复用功能映射值为 AF9,而大部分 LTDC 的信号线都是 AF14,见图 27-26,在编写宏的时候要注意区分。 第 350 页 共 928 零死角玩转 STM32—F429 初始化 LTDC 的 GPIO 图 27-26 LTDC 的复用功能映射 利用上面的宏,编写 LTDC 的 GPIO 引脚初始化函数,见代码清单 24-3。 代码清单 27-5 LTDC 的 GPIO 初始化函数(省略了部分数据线) 1 /** 2 * @brief 初始化控制 LCD 的 IO 3 * @param 无 4 * @retval 无 5 */ 6 static void LCD_GPIO_Config(void) 7{ 8 GPIO_InitTypeDef GPIO_InitStruct; 9 10 /* 使能 LCD 使用到的引脚时钟 */ 11 //红色数据线 /*此处省略部分信号线......*/ 12 RCC_AHB1PeriphClockCmd(LTDC_R0_GPIO_CLK | 13 //控制信号线 14 LTDC_CLK_GPIO_CLK | LTDC_HSYNC_GPIO_CLK |LTDC_VSYNC_GPIO_CLK| 15 LTDC_DE_GPIO_CLK | LTDC_BL_GPIO_CLK |LTDC_DISP_GPIO_CLK ,ENABLE); 16 17 /* GPIO 配置 */ 18 19 /* 红色数据线 */ 20 GPIO_InitStruct.GPIO_Pin = LTDC_R0_GPIO_PIN; 21 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; 22 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; 23 GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; 24 GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL; 25 26 GPIO_Init(LTDC_R0_GPIO_PORT, &GPIO_InitStruct); 27 GPIO_PinAFConfig(LTDC_R0_GPIO_PORT, LTDC_R0_PINSOURCE, LTDC_R0_AF); 28 /*此处省略部分数据信号线......*/ 29 //控制信号线 30 GPIO_InitStruct.GPIO_Pin = LTDC_CLK_GPIO_PIN; 31 GPIO_Init(LTDC_CLK_GPIO_PORT, &GPIO_InitStruct); 32 GPIO_PinAFConfig(LTDC_CLK_GPIO_PORT, LTDC_CLK_PINSOURCE, LTDC_CLK_AF); 第 351 页 共 928 零死角玩转 STM32—F429 33 34 GPIO_InitStruct.GPIO_Pin = LTDC_HSYNC_GPIO_PIN; 35 GPIO_Init(LTDC_HSYNC_GPIO_PORT, &GPIO_InitStruct); 36GPIO_PinAFConfig(LTDC_HSYNC_GPIO_PORT, LTDC_HSYNC_PINSOURCE, LTDC_HSYNC_AF); 37 38 GPIO_InitStruct.GPIO_Pin = LTDC_VSYNC_GPIO_PIN; 39 GPIO_Init(LTDC_VSYNC_GPIO_PORT, &GPIO_InitStruct); 40GPIO_PinAFConfig(LTDC_VSYNC_GPIO_PORT, LTDC_VSYNC_PINSOURCE, LTDC_VSYNC_AF); 41 42 GPIO_InitStruct.GPIO_Pin = LTDC_DE_GPIO_PIN; 43 GPIO_Init(LTDC_DE_GPIO_PORT, &GPIO_InitStruct); 44 GPIO_PinAFConfig(LTDC_DE_GPIO_PORT, LTDC_DE_PINSOURCE, LTDC_DE_AF); 45 46 //背光 BL 及液晶使能信号 DISP 47 GPIO_InitStruct.GPIO_Pin = LTDC_DISP_GPIO_PIN; 48 GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; 49 GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT; 50 GPIO_InitStruct.GPIO_OType = GPIO_OType_PP; 51 GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; 52 53 GPIO_Init(LTDC_DISP_GPIO_PORT, &GPIO_InitStruct); 54 55 GPIO_InitStruct.GPIO_Pin = LTDC_BL_GPIO_PIN; 56 GPIO_Init(LTDC_BL_GPIO_PORT, &GPIO_InitStruct); 57 58 //拉高使能 lcd 59 GPIO_SetBits(LTDC_DISP_GPIO_PORT,LTDC_DISP_GPIO_PIN); 60 GPIO_SetBits(LTDC_BL_GPIO_PORT,LTDC_BL_GPIO_PIN); 61 } 与所有使用到 GPIO 的外设一样,都要先把使用到的 GPIO 引脚模式初始化,以上代 码把 LTDC 的信号线全都初始化为 LCD 复用功能,而背光 BL 及液晶使能 DISP 信号则被 初始化成普通的推挽输出模式,并且在初始化完毕后直接控制它们开启背光及使能液晶屏。 配置 LTDC 的模式 接下来需要配置 LTDC 的工作模式,这个函数的主体是根据液晶屏的硬件特性,设置 LTDC 与液晶屏通讯的时序参数及信号有效极性。代码清单 24-4。 代码清单 27-6 配置 LTDC 的模式 1 /*根据液晶数据手册的参数配置*/ 2 #define HBP 46 //HSYNC 后的无效像素 3 #define VBP 23 //VSYNC 后的无效行数 4 5 #define HSW 1 //HSYNC 宽度 6 #define VSW 1 //VSYNC 宽度 7 8 #define HFP 16 //HSYNC 前的无效像素 9 #define VFP 7 //VSYNC 前的无效行数 10 /* LCD Size (Width and Height) */ 11 #define LCD_PIXEL_WIDTH ((uint16_t)800) 12 #define LCD_PIXEL_HEIGHT ((uint16_t)480) 13 14 /** 15 * @brief LCD 参数配置 16 * @note 这个函数用于配置 LTDC 外设: 17 */ 18 void LCD_Init(void) 19 { 20 LTDC_InitTypeDef LTDC_InitStruct; 21 第 352 页 共 928 零死角玩转 STM32—F429 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 } /* 使能 LTDC 外设时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_LTDC, ENABLE); /* 使能 DMA2D 时钟 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2D, ENABLE); /* 初始化 LCD 的控制引脚 */ LCD_GPIO_Config(); /* 初始化 SDRAM,以便使用 SDRAM 作显存 */ SDRAM_Init(); /* 配置 PLLSAI 分频器,它的输出作为像素同步时钟 CLK*/ /* PLLSAI_VCO 输入时钟 = HSE_VALUE/PLL_M = 1 Mhz */ /* PLLSAI_VCO 输出时钟 = PLLSAI_VCO 输入 * PLLSAI_N = 420 Mhz */ /* PLLLCDCLK = PLLSAI_VCO 输出/PLLSAI_R = 420/6 Mhz */ /* LTDC 时钟频率 = PLLLCDCLK / RCC_PLLSAI = 420/6/8 = 8.75 Mhz */ /* LTDC 时钟太高会导花屏,若对刷屏速度要求不高,降低时钟频率可减少花屏现象*/ /* 以下函数三个参数分别为:PLLSAIN,PLLSAIQ,PLLSAIR,其中 PLLSAIQ 与 LTDC 无关 RCC_PLLSAIConfig(420, 7, 6); RCC_LTDCCLKDivConfig(RCC_PLLSAIDivR_Div8); //这个函数的参数值为 DIV /* 使能 PLLSAI 时钟 */ RCC_PLLSAICmd(ENABLE); /* 等待 PLLSAI 初始化完成 */ while (RCC_GetFlagStatus(RCC_FLAG_PLLSAIRDY) == RESET); /* LTDC 配置*********************************************************/ /*信号极性配置*/ /* 行同步信号极性 */ LTDC_InitStruct.LTDC_HSPolarity = LTDC_HSPolarity_AL; /* 垂直同步信号极性 */ LTDC_InitStruct.LTDC_VSPolarity = LTDC_VSPolarity_AL; /* 数据使能信号极性 */ LTDC_InitStruct.LTDC_DEPolarity = LTDC_DEPolarity_AL; /* 像素同步时钟极性 */ LTDC_InitStruct.LTDC_PCPolarity = LTDC_PCPolarity_IPC; /* 配置 LCD 背景颜色 */ LTDC_InitStruct.LTDC_BackgroundRedValue = 0; LTDC_InitStruct.LTDC_BackgroundGreenValue = 0; LTDC_InitStruct.LTDC_BackgroundBlueValue = 0; /* 时间参数配置 */ /* 配置行同步信号宽度(HSW-1) */ LTDC_InitStruct.LTDC_HorizontalSync =HSW-1; /* 配置垂直同步信号宽度(VSW-1) */ LTDC_InitStruct.LTDC_VerticalSync = VSW-1; /* 配置(HSW+HBP-1) */ LTDC_InitStruct.LTDC_AccumulatedHBP =HSW+HBP-1; /* 配置(VSW+VBP-1) */ LTDC_InitStruct.LTDC_AccumulatedVBP = VSW+VBP-1; /* 配置(HSW+HBP+有效像素宽度-1) */ LTDC_InitStruct.LTDC_AccumulatedActiveW = HSW+HBP+LCD_PIXEL_WIDTH-1; /* 配置(VSW+VBP+有效像素高度-1) */ LTDC_InitStruct.LTDC_AccumulatedActiveH = VSW+VBP+LCD_PIXEL_HEIGHT-1; /* 配置总宽度(HSW+HBP+有效像素宽度+HFP-1) */ LTDC_InitStruct.LTDC_TotalWidth =HSW+ HBP+LCD_PIXEL_WIDTH + HFP-1; /* 配置总高度(VSW+VBP+有效像素高度+VFP-1) */ LTDC_InitStruct.LTDC_TotalHeigh =VSW+ VBP+LCD_PIXEL_HEIGHT + VFP-1; LTDC_Init(<DC_InitStruct); LTDC_Cmd(ENABLE); 该函数的执行流程如下: 第 353 页 共 928 零死角玩转 STM32—F429 (4) 初始化 GPIO 引脚以及 LTDC、DMA2D 时钟 函数开头调用了前面定义的 LCD_GPIO_Config 函数对液晶屏用到的 GPIO 进行初始化, 并且使用库函数 RCC_APB2PeriphClockCmd 及 RCC_AHB1PeriphClockCmd 使能 LTDC 和 DMA2D 外设的时钟。 (5) 初始化 SDRAM 接下来调用前面章节讲解的 SDRAM_Init 函数初始化 FMC 外设控制 SDRAM,以便使 用 SDRAM 的存储空间作为显存。 (6) 设置像素同步时钟 在“LTDC 结构框图的时钟信号”小节讲解到,LTDC 与液晶屏通讯的像素同步时钟 CLK 是由 PLLSAI 分频器控制输出的,它的时钟源为外部高速晶振 HSE 经过分频因子 M 分频后的时钟,按照默认设置,一般分频因子 M 会把 HSE 分频得到 1MHz 的时钟, 如 HSE 晶振频率为 25MHz 时,把 M 设置为 25,HSE 晶振频率为 8MHz 时,把 M 设 置为 8,然后调用 SystemInit 函数初始化系统时钟。经过 M 分频得到的 1MHz 时钟输 入到 PLLSAI 分频器后,使用倍频因子“N”倍频,然后再经过“R”因子分频,得到 PLLCDCLK 时钟,再由“DIV”因子分频得到 LTDC 通讯的同步时钟 LCD_CLK。 即:fLCD_CLK=fHSE/M x N/R/DIV 由于 M 把 HSE 时钟分频为 1MHz 的时钟,所以上式等价于: fLCD_CLK=1xN/R/DIV 利用库函数 RCC_PLLSAIConfig 及 RCC_LTDCCLKDivConfig 函数可以配置 PLLSAI 分频器的这些参数,其中库函数 RCC_PLLSAIConfig 的三个输入参数分别是倍频因子 N、分频因子 Q 和分频因子 R,其中“Q”因子是作用于 SAI 接口的分频时钟,与 LTDC 无关,RCC_LTDCCLKDivConfig 函数的输入参数为分频因子“DIV”。在配置 完这些分频参数后,需要调用库函数 RCC_PLLSAICmd 使能 PLLSAI 的时钟并且检测 标志位等待时钟初始化完成。 在上面的代码中调用函数设置 N=420,R=6,DIV=8,计算得 LCD_CLK 的时钟频率 为 8.75MHz,这个时钟频率是我们根据实测效果选定的,若使用的是 16 位数据格式, 可把时钟频率设置为 24MHz,若只使用单层液晶屏数据源,则可配置为 34MHz。然 而根据液晶屏的数据手册查询可知它支持最大的同步时钟为 50MHz,典型速率为 33.3Mhz,见图 27-28,由此说明传输速率主要受限于 STM32 一方。LTDC 外设需要从 SDRAM 显存读取数据,这会消耗一定的时间,所以使用 32 位像素格式的数据要比使 用 16 位像素格式的慢,如若只使用单层数据源,还可以进一步减少一半的数据量,所 以更快。 (7) 配置信号极性 接下来根据液晶屏的时序要求,配置 LTDC 与液晶屏通讯时的信号极性,见图 27-27。 在程序中配置的 HSYNC、VSYNC、DE 有效信号极性均为低电平,同步时钟信号极 性配置为上升沿。其中 DE 信号的极性跟液晶屏时序图的要求不一样,文档中 DE 的 有效电平为高电平,而实际测试中把设置为 DE 低电平有效时屏幕才能正常工作,我 们以实际测试为准。 第 354 页 共 928 零死角玩转 STM32—F429 图 27-27 液晶屏时序中的有效电平 (8) 配置时间参数 液晶屏通讯中还有时间参数的要求,接下来的程序我们根据液晶屏手册给出的时间参 数,配置 HSW、VSW、HBP、HFP、VBP、VFP、有效像素宽度及有效行数。这些参 数都根据图 27-28 的说明以宏定义在程序中给出。 第 355 页 共 928 零死角玩转 STM32—F429 图 27-28 液晶屏数据手册标注的时间参数 (9) 写入参数到寄存器并使能外设 经过上面步骤,赋值完了初始化结构体,接下来调用库函数 LTDC_Init 把各种参数写 入到 LTDC 的控制寄存器中,然后调用库函数 LTDC_Cmd 使能 LTDC。 配置 LTDC 的层级初始化 在上面配置完成 STM32 的 LTDC 外设基本工作模式后,还需要针对液晶屏的各个数 据源层进行初始化,才能正常工作,代码清单 44-7。 代码清单 27-7 LTDC 的层级初始化 1 /* LCD Size (Width and Height) */ 2 #define LCD_PIXEL_WIDTH ((uint16_t)800) 3 #define LCD_PIXEL_HEIGHT ((uint16_t)480) 4 5 #define LCD_FRAME_BUFFER ((uint32_t)0xD0000000) 6 #define BUFFER_OFFSET 7 #define LCD_PIXCELS 8 /** 9 * @brief 初始化 LTD 的 层 参数 10 * - 设置显存空间 11 * - 设置分辨率 12 * @param None ((uint32_t)800*480*3) ((uint32_t)800*480) 13 * @retval None //第一层首地址 //一层液晶的数据量 第 356 页 共 928 零死角玩转 STM32—F429 14 */ 15 void LCD_LayerInit(void) 16 { 17 LTDC_Layer_InitTypeDef LTDC_Layer_InitStruct; 18 19 /* 层窗口配置 */ 20 /* 配置本层的窗口边界,注意这些参数是包含 HBP HSW VBP VSW 的 */ 21 //一行的第一个起始像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedHBP+1)的 值 22 LTDC_Layer_InitStruct.LTDC_HorizontalStart = HBP + HSW; 23 //一行的最后一个像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedActiveW)的 值 24 LTDC_Layer_InitStruct.LTDC_HorizontalStop = HSW+HBP+LCD_PIXEL_WIDTH-1; 25 //一列的第一个起始像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedVBP+1)的 值 26 LTDC_Layer_InitStruct.LTDC_VerticalStart = VBP + VSW; 27 //一列的最后一个像素,该值应为 (LTDC_InitStruct.LTDC_AccumulatedActiveH)的 值 28 LTDC_Layer_InitStruct.LTDC_VerticalStop = VSW+VBP+LCD_PIXEL_HEIGHT-1; 29 30 /* 像素格式配置*/ 31 LTDC_Layer_InitStruct.LTDC_PixelFormat = LTDC_Pixelformat_RGB888; 32 33 /* 默认背景颜色,该颜色在定义的层窗口外或在层禁止时使用。 */ 34 LTDC_Layer_InitStruct.LTDC_DefaultColorBlue = 0; 35 LTDC_Layer_InitStruct.LTDC_DefaultColorGreen = 0; 36 LTDC_Layer_InitStruct.LTDC_DefaultColorRed = 0; 37 LTDC_Layer_InitStruct.LTDC_DefaultColorAlpha = 0; 38 /* 恒定 Alpha 值配置,0-255 */ 39 40 值 */ LTDC_Layer_InitStruct.LTDC_ConstantAlpha = 255; /* 配置混合因子 CA 表示使用恒定 Alpha 值,PAxCA 表示使用像素 Alpha x 恒定 Alpha 41 LTDC_Layer_InitStruct.LTDC_BlendingFactor_1 = LTDC_BlendingFactor1_CA; 42 LTDC_Layer_InitStruct.LTDC_BlendingFactor_2 = LTDC_BlendingFactor2_CA; 43 44 /* 该成员应写入(一行像素数据占用的字节数+3) 45 Line Lenth = 行有效像素个数 x 每个像素的字节数 + 3 46 行有效像素个数 = LCD_PIXEL_WIDTH 47 每个像素的字节数 = 2(RGB565/RGB1555)/ 3 (RGB888)/ 4(ARGB8888) 48 */ 49 LTDC_Layer_InitStruct.LTDC_CFBLineLength = ((LCD_PIXEL_WIDTH * 3) + 3); 50 /* 从某行的起始位置到下一行起始位置处的像素增量 51 Pitch = 行有效像素个数 x 每个像素的字节数 */ 52 LTDC_Layer_InitStruct.LTDC_CFBPitch = (LCD_PIXEL_WIDTH * 3); 53 54 /* 配置有效的行数 */ 55 LTDC_Layer_InitStruct.LTDC_CFBLineNumber = LCD_PIXEL_HEIGHT; 56 /* 配置本层的显存首地址 */ 57 LTDC_Layer_InitStruct.LTDC_CFBStartAdress = LCD_FRAME_BUFFER; 58 /* 以上面的配置初始化第 1 层*/ 59 LTDC_LayerInit(LTDC_Layer1, <DC_Layer_InitStruct); 60 61 /*配置第 2 层,若没有重写某个成员的值,则该成员使用跟第 1 层一样的配置 */ 62 /* 配置本层的显存首地址,这里配置它紧挨在第 1 层的后面*/ 63 LTDC_Layer_InitStruct.LTDC_CFBStartAdress = LCD_FRAME_BUFFER+ BUFFER_OFFSET; 64 65 /* 配置混合因子,使用像素 Alpha 参与混合 */ 66 LTDC_Layer_InitStruct.LTDC_BlendingFactor_1 = LTDC_BlendingFactor1_PAxCA; 67 LTDC_Layer_InitStruct.LTDC_BlendingFactor_2 = LTDC_BlendingFactor2_PAxCA; 68 /* 初始化第 2 层 */ 69 LTDC_LayerInit(LTDC_Layer2, <DC_Layer_InitStruct); 70 第 357 页 共 928 零死角玩转 STM32—F429 71 72 73 74 75 76 77 78 79 } 80 /* 立即重载配置 */ LTDC_ReloadConfig(LTDC_IMReload); /*使能前景及背景层 */ LTDC_LayerCmd(LTDC_Layer1, ENABLE); LTDC_LayerCmd(LTDC_Layer2, ENABLE); /* 立即重载配置 */ LTDC_ReloadConfig(LTDC_IMReload); LTDC 的层级初始化函数执行流程如下: (1) 配置窗口边界 每层窗口都需要配置有效显示窗口,使用 LTDC_HorizontalStart/ HorizontalStop/ LTDC_VerticalStart/ LTDC_VerticalStop 成员来确定这个窗口的左右上下边界,各个成 员应写入的值与前面 LTDC 初始化结构体中某些参数类似,注意某些成员要求加 1 或 减 1。 (2) 配置像素的格式 LTDC_PixelFormat 成员用于配置本层像素的格式,在这个实验中我们把这层设置为 RGB888 格式,两层数据源的像素可以配置成不同的格式,层与层之间是独立的。 (3) 配置默认背景颜色 在定义的层窗口外或在层禁止时,该层会使用默认颜色作为数据源,默认颜色使用 LTDC_DefaultColorBlue/Green/Red/Alpha 成员来配置,本实验中我们把默认颜色配置 成透明了。 (4) 配置第 1 层的恒定 Alpha 与混合因子 前面提到两层数据源混合时可根据混合因子设置只使用恒定 Alpha 运算,还是把像素 的 Alpha 也加入到运算中。对于第 1 层数据源,我们不希望 LTDC 的默认背景层参与 到混合运算中,而希望第 1 层直接作为背景(因为第 1 层的数据每个像素点都是可控的, 而背景层所有像素点都是同一个颜色)。因此我们把恒定 Alpha 值 (LTDC_ConstantAlpha)设置为 255,即完全不透明,混合因子 BF1/BF2 参数 (LTDC_BlendingFactor_1/2)都配置成 LTDC_BlendingFactor1/2_CA,即只使用恒定 Alpha 值运算,这样配置的结果是第 1 层的数据颜色直接等于它像素本身的 RGB 值, 不受像素中的 Alpha 值及背景影响。 (5) 配置层的数据量 通过参数 LTDC_CFBLineLength 及 LTDC_CFBLineNumber 可设定层的数据量,层的 数据量跟显示窗口大小及像素格式有关,由于我们把这层的像素格式设置成了 RGB888,所以每个像素点的大小为 3 字节,LTDC_CFBLineLength 参数写入值:行有 效像素个数 x3+3 。而 LTDC_CFBLineNumber 参数直接写入有效行数 (LCD_PIXEL_HEIGHT)。还有一个参数 LTDC_CFBPitch 它用于配置上一行起始像素 与下一行起始像素的数据增量,我们直接写入:行有效像素个数 x3 即可。 (6) 配置显存首地址 每一层都有独立的显存空间,向 LTDC_CFBStartAdress 参数赋值可设置该层的显 存首地址,我们把第 1 层的显存首地址直接设置成宏 LCD_FRAME_BUFFER,该宏表 第 358 页 共 928 零死角玩转 STM32—F429 示的地址为 0xD0000000,即 SDRAM 的首地址,从该地址开始,BUFFER_OFFSET 个字节的空间都用作这一层的显存空间(BUFFER_OFFSET 宏的值为 800x480x3:行有效 像素宽度 x 行数 x 每个字节的数据量),向这些空间写入的数据会被解释成像素数据, LTDC 会把这些数据传输到液晶屏上,所以我们要控制液晶屏的输出,只要修改这些 空间的数据即可,包括变量操作、指针操作、DMA 操作以及 DMA2D 操作等一切可修 改 SDRAM 内容的操作都支持。 实际设置中不需要刻意设置成 SDRAM 首地址,只要能保证该地址后面的数据空 间足够存储该层的一帧数据即可。 (7) 向寄存器写入配置参数 赋值完后,调用库函数 LTDC_LayerInit 可把这些参数写入到 LTDC 的层控制寄存器, 根据函数的第一个参数 LTDC_Layer1/2 来决定配置的是第 1 层还是第 2 层。 (8) 配置第 2 层控制参数 要想有混合效果,还需要使用第 2 层数据源,它与第 1 层的配置大致是一样的, 主要区别是显存首地址和混合因子。在程序中我们把第 2 层的显存首地址设置成紧挨 着第 1 层显存空间的结尾。而混合因子都配置成 PAxCA 以便它的透明像素能参与运 算,实现透明效果,但实际上我们并没有修改第 2 层像素数据的格式,它依然使用 RGB888 格式,由于像素本身并没有 Alpha 通道的数据,所以是没有透明混合效果的。 正常使用时可把第 2 层配置成 ARGB8888 或 ARGB1555 格式,才能正常使用两层 数据混合的功能。本程序没有配置透明格式主要是因为各种描绘函数(如画点、画线等) 是要根据像素格式进行修改的。两层使用不同的像素格式那么就要有两套同样功能的 函数,容易造成混乱,而 ARGB8888 的数据量太大,所以我们把两层的像素格式都设 置成了 RGB888。 如果想了解使用透明像素格式如何使用,可把工程里的“bsp_lcd.h”文件的宏 “LCD_RGB888”值修改为 0,这样“bsp_lcd.c”文件会使用两层都配置为 ARGB1555 的格式及控制函数: 1 /*把这个宏设置成非 0 值 液晶屏使用 RGB888 色彩,若为 0 则使用 ARGB1555 色彩*/ 2 #define LCD_RGB_888 1 (9) 重载 LTDC 配置并使能数据层 把两层的参数都写入到寄存器后,使用库函数 LTDC_ReloadConfig 让 LTDC 外设立即 重新加载这些配置,并使用库函数 LTDC_LayerCmd 使能两层的数据源。至此,LTDC 配置就完成,可以向显存空间写入数据进行显示了。 辅助显示的全局变量及函数 为方便显示操作,我们定义了一些全局变量及函数来辅助修改显存内容,这些函数都 是我们自己定义的,不是 STM32 标准库提供的内容。见代码清单 27-8。 代码清单 27-8 辅助显示的全局变量及函数 1 2 /*字体格式*/ 3 typedef struct _tFont 4{ 第 359 页 共 928 零死角玩转 STM32—F429 5 const uint16_t *table; /*指向字模数据的指针*/ 6 uint16_t Width; /*字模的像素宽度*/ 7 uint16_t Height; /*字模的像素高度*/ 8 } sFONT; 9 /*这些可选的字体格式定义在 fonts.c 文件*/ 10 extern sFONT Font16x24; 11 extern sFONT Font12x12; 12 extern sFONT Font8x12; 13 extern sFONT Font8x8; 14 15 /** 16 * @brief LCD Layer 17 */ 18 #define LCD_BACKGROUND_LAYER 0x0000 19 #define LCD_FOREGROUND_LAYER 0x0001 20 21 #define LCD_FRAME_BUFFER ((uint32_t)0xD0000000) //第一层首地址 22 #define BUFFER_OFFSET ((uint32_t)800*480*3) //一层液晶的数据量 23 24 /*用于存储当前选择的字体格式*/ 25 static sFONT *LCD_Currentfonts; 26 /* 用于存储当前字体颜色和字体背景颜色的变量*/ 27 static uint32_t CurrentTextColor = 0x000000; 28 static uint32_t CurrentBackColor = 0xFFFFFF; 29 /* 用于存储层对应的显存空间 和 当前选择的层*/ 30 static uint32_t CurrentFrameBuffer = LCD_FRAME_BUFFER; 31 static uint32_t CurrentLayer = LCD_BACKGROUND_LAYER; 32 33 /** 34 * @brief 设置字体格式(英文) 35 * @param fonts: 选择要设置的字体格式 36 * @retval None 37 */ 38 void LCD_SetFont(sFONT *fonts) 39 { 40 LCD_Currentfonts = fonts; 41 } 42 /** 43 * @brief 选择要控制的层. 44 * @param Layerx: 选择要操作前景层(第 2 层)还是背景层(第 1 层) 45 * @retval None 46 */ 47 void LCD_SetLayer(uint32_t Layerx) 48 { 49 if (Layerx == LCD_BACKGROUND_LAYER) 50 { 51 CurrentFrameBuffer = LCD_FRAME_BUFFER; 52 CurrentLayer = LCD_BACKGROUND_LAYER; 53 } 54 else 55 { 56 CurrentFrameBuffer = LCD_FRAME_BUFFER + BUFFER_OFFSET; 57 CurrentLayer = LCD_FOREGROUND_LAYER; 58 } 59 } 60 61 /** 62 * @brief 设置字体的颜色及字体的背景颜色 63 * @param TextColor: 字体颜色 64 * @param BackColor: 字体的背景颜色 65 * @retval None 66 */ 67 void LCD_SetColors(uint32_t TextColor, uint32_t BackColor) 68 { 69 CurrentTextColor = TextColor; 第 360 页 共 928 零死角玩转 STM32—F429 70 CurrentBackColor = BackColor; 71 } 72 73 /** 74 * @brief 设置字体颜色 75 * @param Color: 字体颜色 76 * @retval None 77 */ 78 void LCD_SetTextColor(uint32_t Color) 79 { 80 CurrentTextColor = Color; 81 } 82 83 /** 84 * @brief 设置字体的背景颜色 85 * @param Color: 字体的背景颜色 86 * @retval None 87 */ 88 void LCD_SetBackColor(uint32_t Color) 89 { 90 CurrentBackColor = Color; 91 } (1) 切换字体大小格式 液晶显示中,文字内容占据了很大部分,显示文字需要有“字模数据”配合。关于字 模的知识我们在下一章节讲解,在这里只简单介绍一下基本概念。字模是一个个像素 点阵方块 ,如上述代码中的 sFont 结构体,包含了指向字模数据的指针以及每个字模 的像素宽度、高度,即字体的大小。本实验的工程中提供了像素格式为 16x24、12x12、 8x12、8x8 的英文字模。为了方便选择字模,定义了全局指针变量 LCD_Currentfonts 用来存储当前选择的字模格式,实际显示时根据该指针指向的字模格式来显示文字, 可以使用下面的 LCD_SetFont 函数切换指针指向的字模格式,该函数的可输入参数为: Font16x24/ Font12x12/ Font8x12/ Font8x8。 (2) 切换字体颜色和字体背景颜色 很多时候我们还希望文字能以不同的色彩显示,为此定义了全局变量 CurrentTextColor 和 CurrentBackColor 用于设定要显示字体的颜色和字体背景颜色,如: 字体为红色和字体背景为蓝色 使用函数 LCD_SetColors、LCD_SetTextColor 以及 LCD_SetBackColor 可以方便修改这 两个全局变量的值。若液晶的像素格式支持透明,可把字体背景设置为透明值,实现 弹幕显示的效果(文字浮在图片之上,透过文字可看到背景图片)。 (3) 切换当前操作的液晶层 由于显示的数据源有两层,在写入数据时需要区分到底要写入哪个显存空间,为此, 我们定义了全局变量 CurrentLayer 和 CurrentFrameBuffer 用于存储要操作的液晶层及该 层的显存首地址。使用函数 LCD_SetLayer 可切换要操作的层及显存地址。 绘制像素点 有了以上知识准备,就可以开始向液晶屏绘制像素点了,见代码清单 27-9。 代码清单 27-9 绘制像素点 1 /** 2 * @brief 显示一个像素点 3 * @param x: 像素点的 x 坐标 第 361 页 共 928 零死角玩转 STM32—F429 4 * @param y: 像素点的 y 坐标 5 * @retval None 6 */ 7 void PutPixel(int16_t x, int16_t y) 8{ 9 if (x < 0 || x > LCD_PIXEL_WIDTH || y < 0 || y > LCD_PIXEL_HEIGHT) 10 { 11 return; 12 } 13 { 14 /*RGB888*/ 15 uint32_t Xaddress =0; 16 Xaddress = CurrentFrameBuffer + 3*(LCD_PIXEL_WIDTH*y + x); 17 *(__IO uint16_t*) Xaddress= (0x00FFFF & CurrentTextColor); //GB 18 *(__IO uint8_t*)( Xaddress+2)= (0xFF0000 & CurrentTextColor) >> 16; //R 19 } 20 } 这个绘制像素点的函数可输入 x,y 两个参数,用于指示要绘制像素点的坐标。得到输 入参数后它首先进行参数检查,若坐标超出液晶显示范围则直接退出函数,不进行操作。 坐标检查通过后根据坐标计算该像素所在的显存地址,液晶屏中的每个像素点都有对应的 显存空间,像素点的坐标与显存地址有固定的映射关系,见表 27-8。 表 27-8 显存存储像素数据的方式 (RGB888 格式) … 2 1 0 „ Bx+2[7:0] Rx+1[7:0] Gx+1[7:0] Bx+1[7:0] Rx[7:0] Gx[7:0] Bx[7:0] 行/列 … 6 5 4 3 2 1 0 当像素格式为 RGB888 时,每个像素占据 3 个字节,各个像素点按顺序排列。而且 RGB 通道的数据各占一个字节空间,蓝色数据存储在低位的地址,红色数据存储右高位地 址。据此可以得出像素点显存地址与像素点坐标存在以下映射关系: 像素点的显存基地址= 当前层显存首地址 + 每个像素点的字节数*(每行像素个数*坐标 y+坐标 x) 而像素点内的 RGB 颜色分量地址如下: 蓝色分量地址 = 像素点显存基地址 绿色分量地址 = 像素点显存基地址+1 红色分量地址 = 像素点显存基地址+2 利用这些映射关系,绘制点函数代入存储了当前要操作的层显存首地址的全局变量 CurrentFrameBuffer 计算出像素点的显存基地址 Xaddress,再利用指针操作把当前字体颜色 CurrentTextColor 中的 RGB 颜色分量分别存储到对应的位置。由于 LTDC 工作后会一直刷 新显存的数据到液晶屏,所以在下一次 LTDC 刷新的时候,被修改的显存数据就会显示到 液晶屏上了。 掌握了绘制任意像素点颜色的操作后,就能随心所欲地控制液晶屏了,其它复杂的显 示操作如绘制直线、矩形、圆形、文字、图片以及视频都是一样的,本质上都是操纵一个 个像素点而已。如直线由点构成,矩形由直线构成,它们的区别只是点与点之间几何关系 的差异,对液晶屏来说并没有什么特别。 第 362 页 共 928 零死角玩转 STM32—F429 使用 DMA2D 绘制直线和矩形 利用上面的像素点绘制方式可以实现所有液晶操作,但直接使用指针访问内存空间效 率并不高,在某些场合下可使用 DMA2D 搬运内存数据,加速传输。绘制纯色直线和矩形 的时候十分适合,代码清单 27-10。 代码清单 27-10 使用 DMA2D 绘制直线 1 /** 2 * @brief LCD Direction 3 */ 4 #define LCD_DIR_HORIZONTAL 0x0000 5 #define LCD_DIR_VERTICAL 0x0001 6 /** 7 * @brief 显示一条直线 8 * @param Xpos: 直线起点的 x 坐标 9 * @param Ypos: 直线起点的 y 坐标 10 * @param Length: 直线的长度 11 * @param Direction: 直线的方向,可输入 LCD_DIR_HORIZONTAL(水平方向) 12 LCD_DIR_VERTICAL(垂直方向). 13 * @retval None 14 */ 15 void LCD_DrawLine(uint16_t Xpos, uint16_t Ypos, uint16_t Length, 16 uint8_t Direction) 17 { 18 DMA2D_InitTypeDef DMA2D_InitStruct; 19 20 uint32_t Xaddress = 0; 21 uint16_t Red_Value = 0, Green_Value = 0, Blue_Value = 0; 22 23 /*计算目标地址*/ 24 Xaddress = CurrentFrameBuffer + 3*(LCD_PIXEL_WIDTH*Ypos + Xpos); 25 26 /*提取颜色分量*/ 27 Red_Value = (0xFF0000 & CurrentTextColor) >>16; 28 Blue_Value = 0x0000FF & CurrentTextColor; 29 Green_Value = (0x00FF00 & CurrentTextColor)>>8 ; 30 31 /* 配置 DMA2D */ 32 DMA2D_DeInit(); 33 DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M; 34 DMA2D_InitStruct.DMA2D_CMode = DMA2D_RGB888; 35 DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value; 36 DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value; 37 DMA2D_InitStruct.DMA2D_OutputRed = Red_Value; 38 DMA2D_InitStruct.DMA2D_OutputAlpha = 0x0F; 39 DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress; 40 41 /*水平方向*/ 42 if (Direction == LCD_DIR_HORIZONTAL) 43 { 44 DMA2D_InitStruct.DMA2D_OutputOffset = 0; 45 DMA2D_InitStruct.DMA2D_NumberOfLine = 1; 46 DMA2D_InitStruct.DMA2D_PixelPerLine = Length; 47 } 48 else /*垂直方向*/ 49 { 50 DMA2D_InitStruct.DMA2D_OutputOffset = LCD_PIXEL_WIDTH - 1; 51 DMA2D_InitStruct.DMA2D_NumberOfLine = Length; 52 DMA2D_InitStruct.DMA2D_PixelPerLine = 1; 53 } 54 DMA2D_Init(&DMA2D_InitStruct); 55 /*开始 DMA2D 传输 */ 56 DMA2D_StartTransfer(); 第 363 页 共 928 零死角玩转 STM32—F429 57 58 59 } /*等待传输结束 */ while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET); 这个绘制直线的函数输入参数为直线起始像素点的坐标,直线长度,以及直线的方向 (它只能描绘水平直线或垂直直线),函数主要利用了前面介绍的 DMA2D 初始化结构体, 执行流程介绍如下: (1) 计算起始像素点的显存位置 与绘制单个像素点一样,使用 DMA2D 绘制也需要知道像素点对应的显存地址。利用 直线起始像素点的坐标计算出直线在显存的基本位置 Xaddress。 (2) 提取直线颜色的 RGB 分量 使用 DMA2D 绘制纯色数据时,需要向它的寄存器写入 RGB 通道的数据,所以我们先 把直线颜色 CurrentTextColor 的 RGB 分量提取到 RED/ Green / Blue _Value 变量值中。 (3) 配置 DMA2D 传输模式像素格式、颜色分量及偏移地址。 接下来开始向 DMA2D 初始化结构体赋值,在赋值前先调用了库函数 DMA2D_DeInit, 以便关闭 DMA2D,因为它只有在非工作状态下才能重新写入配置。配置时把 DMA2D 的模式设置成了 DMA2D_R2M,以寄存器中的颜色作为数据源,即 DMA2D_OutputGreen/Blue/Red/Alpha 中的值,我们向这些参数写入上面提取得到的颜 色分量。DMA2D 输出地址设置为上面计算得的 Xaddress。 (4) 配置 DMA2D 的输出偏移、行数及每行的像素点个数 直线方向不同时,对 DMA2D_OutputOffset(行偏移)、DMA2D_NumberOfLine(行的数 量)及 DMA2D_PixelPerLine(每行的像素宽度)这几个参数的配置是不一样的。在显示垂 直线的时候才需要行偏移,而在显示水平线的时候,由于水平线宽度只有一个像素点, 只占据一行,像素点,不需要换行,所以行偏移设置为任意值都不影响。行偏移的概 念比较抽象,请参考前面解释“DMA2D 初始化结构体”的内容理解。 (5) 写入参数到寄存器并传输 配置完 DMA2D 的参数后,调用库函数 DMA2D_Init 把参数写入到寄存器中,然后调 用 DMA2D_StartTransfer 开始传输,然后检测标志位等待传输结束。 使用 DMA2D 绘制矩形 与绘制直线很类似,利用 DMA2D 绘制纯色矩形的方法见代码清单 27-11。 代码清单 27-11 使用 DMA2D 绘制矩形 1 /** 2 * @brief 绘制实心矩形 3 * @param Xpos: 起始 X 坐标 4 * @param Ypos: 起始 Y 坐标 5 * @param Height: 矩形高 6 * @param Width: 矩形宽 7 * @retval None 8 */ 9 void LCD_DrawFullRect(uint16_t Xpos, uint16_t Ypos, uint16_t Width, 10 uint16_t Height) 11 { 12 DMA2D_InitTypeDef DMA2D_InitStruct; 13 14 uint32_t Xaddress = 0; 15 uint16_t Red_Value = 0, Green_Value = 0, Blue_Value = 0; 第 364 页 共 928 零死角玩转 STM32—F429 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 } Red_Value = (0xFF0000 & CurrentTextColor)>>16 ; Blue_Value = 0x0000FF & CurrentTextColor; Green_Value = (0x00FF00 & CurrentTextColor)>>8; Xaddress = CurrentFrameBuffer + 3*(LCD_PIXEL_WIDTH*Ypos + Xpos); /* 配置 DMA2D DMA2D */ DMA2D_DeInit(); DMA2D_InitStruct.DMA2D_Mode = DMA2D_R2M; DMA2D_InitStruct.DMA2D_CMode = DMA2D_RGB888; DMA2D_InitStruct.DMA2D_OutputGreen = Green_Value; DMA2D_InitStruct.DMA2D_OutputBlue = Blue_Value; DMA2D_InitStruct.DMA2D_OutputRed = Red_Value; DMA2D_InitStruct.DMA2D_OutputAlpha = 0x0F; DMA2D_InitStruct.DMA2D_OutputMemoryAdd = Xaddress; DMA2D_InitStruct.DMA2D_OutputOffset = (LCD_PIXEL_WIDTH - Width); DMA2D_InitStruct.DMA2D_NumberOfLine = Height; DMA2D_InitStruct.DMA2D_PixelPerLine = Width; DMA2D_Init(&DMA2D_InitStruct); /* 开始 DMA2D 传输 */ DMA2D_StartTransfer(); /* 等待传输结束 */ while (DMA2D_GetFlagStatus(DMA2D_FLAG_TC) == RESET); 对于 DMA2D 来说,绘制矩形实际上就是绘制一条很粗的直线,与绘制直线的主要区 别是行偏移、行数以及每行的像素个数。 3. main 函数 最后我们来编写 main 函数,使用液晶屏显示图像,见代码清单 24-14。 代码清单 27-12 main 函数 1 /** 2 * @brief 主函数 3 * @param 无 4 * @retval 无 5 */ 6 int main(void) 7{ 8 /* LED 端口初始化 */ 9 LED_GPIO_Config(); 10 11 /*初始化液晶屏*/ 12 LCD_Init(); 13 LCD_LayerInit(); 14 LTDC_Cmd(ENABLE); 15 16 /*把背景层刷黑色*/ 17 LCD_SetLayer(LCD_BACKGROUND_LAYER); 18 LCD_Clear(LCD_COLOR_BLACK); 19 20 /*初始化后默认使用前景层*/ 21 LCD_SetLayer(LCD_FOREGROUND_LAYER); 22 /*默认设置不透明 ,该函数参数为不透明度,范围 0-0xff ,0 为全透明,0xff 为不透明 */ 23 LCD_SetTransparency(0xFF); 24 LCD_Clear(LCD_COLOR_BLACK); 25 /*经过 LCD_SetLayer(LCD_FOREGROUND_LAYER)函数后, 26 以下液晶操作都在前景层刷新,除非重新调用过 LCD_SetLayer 函数设置背景层*/ 27 第 365 页 共 928 零死角玩转 STM32—F429 28 29 30 31 32 33 34 35 36 } LED_BLUE; Delay(0xfff); while (1) { LCD_Test(); } 上电后,调用了 LCD_Init、LCD_LayerInit 函数初始化 LTDC 外设,然后使用 LCD_SetLayer 函数切换到背景层,使用 LCD_Clear 函数把背景层都刷成黑色,LCD_Clear 实质是一个使用 DMA2D 显示矩形的函数,只是它默认矩形的宽和高直接设置成液晶屏的 分辨率,把整个屏幕都刷成同一种颜色。刷完背景层的颜色后再调用 LCD_SetLayer 切换 到前景层,然后在前景层绘制图形。中间还有一个 LCD_SetTransparency 函数,它用于设 置当前层的透明度,设置后整一层的像素包含该透明值,由于整层透明并没有什么用(一般 应用是某些像素点透明看到背景,而其它像素点不透明),我们把前景层设置为完全不透明。 初始化完成后,我们调用 LCD_Test 函数显示各种图形进行测试(如直线、矩形、圆形), 具体内容请直接在工程中阅读源码,这里不展开讲解。LCD_Test 中还调用了文字显示函数, 其原理在下一章节详细说明。 下载验证 用 USB 线连接开发板,编译程序下载到实验板,并上电复位,液晶屏会显示各种内容。 27.9 每课一问 6. 不使用 DMA2D,编写程序绘制纯色直线和纯色矩形,并测试比较它与 DMA2D 方式 的速度差异。(RGB888 像素格式,可参考 PutPixel 函数) 7. 如果像素格式分别为 RGB1555 和 ARGB8888,应如何修改绘制像素点函数(PutPixel) 以及 DMA2D 绘制矩形函数(LCD_DrawFullRect)。 第 366 页 共 928 零死角玩转 STM32—F429 第28章 LTDC—液晶显示中英文 本章参考资料:《STM32F4xx 参考手册 2》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 关于开发板配套的液晶屏参数可查阅《5.0 寸液晶屏数据手册》配套资料获知。 在前面我们学习了如何使用 LTDC 外设控制液晶屏并用它显示各种图形,本章讲解如 何控制液晶屏显示文字。使用液晶屏显示文字时,涉及到字符编码与字模的知识。 28.1 字符编码 由于计算机只能识别 0 和 1,文字也只能以 0 和 1 的形式在计算机里存储,所以我们 需要对文字进行编码才能让计算机处理,编码的过程就是规定特定的 01 数字串来表示特定 的文字,最简单的字符编码例子是 ASCII 码。 28.1.1 ASCII 编码 学习 C 语言时,我们知道在程序设计中使用 ASCII 编码表约定了一些控制字符、英文 及数字。它们在存储器中,本质也是二进制数,只是我们约定这些二进制数可以表示某些 特殊意义,如以 ASCII 编码解释数字“0x41”时,它表示英文字符“A”。ASCII 码表分 为两部分,第一部分是控制字符或通讯专用字符,它们的数字编码从 0~31,见表 28-1,它 们并没有特定的图形显示,但会根据不同的应用程序,而对文本显示有不同的影响。 ASCII 码的第二部分包括空格、阿拉伯数字、标点符号、大小写英文字母以及“DEL(删 除控制)”,这部分符号的数字编码从 32~127,除最后一个 DEL 符号外,都能以图形的 方式来表示,它们属于传统文字书写系统的一部分。 十进制 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 表 28-1 ASCII 码中的控制字符或通讯专用字符 十六进制 缩写/字符 解释 0 NUL(null) 空字符 1 SOH(start of headline) 标题开始 2 STX (start of text) 正文开始 3 ETX (end of text) 正文结束 4 EOT (end of transmission) 传输结束 5 ENQ (enquiry) 请求 6 ACK (acknowledge) 收到通知 7 BEL (bell) 响铃 8 BS (backspace) 退格 9 HT (horizontal tab) 水平制表符 0A LF (NL line feed, new line) 换行键 0B VT (vertical tab) 垂直制表符 0C FF (NP form feed, new page) 换页键 0D CR (carriage return) 回车键 0E SO (shift out) 不用切换 第 367 页 共 928 零死角玩转 STM32—F429 15 0F 16 10 17 11 18 12 19 13 20 14 21 15 22 16 23 17 24 18 25 19 26 1A 27 1B 28 1C 29 1D 30 1E 31 1F 十进制 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 十六进制 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A SI (shift in) DLE (data link escape) DC1 (device control 1) DC2 (device control 2) DC3 (device control 3) DC4 (device control 4) NAK (negative acknowledge) SYN (synchronous idle) ETB (end of trans. block) CAN (cancel) EM (end of medium) 启用切换 数据链路转义 设备控制 1 设备控制 2 设备控制 3 设备控制 4 拒绝接收 同步空闲 传输块结束 取消 介质中断 SUB (substitute) ESC (escape) FS (file separator) 替补 换码(溢出) 文件分割符 GS (group separator) RS (record separator) US (unit separator) 分组符 记录分离符 单元分隔符 表 28-2 ASCII 码中的字符及数字 缩写/字符 十进制 十六进制 缩写/字符 (space)空格 80 50 P ! 81 51 Q " 82 52 R # 83 53 S $ 84 54 T % 85 55 U & 86 56 V ' 87 57 W ( 88 58 X ) 89 59 Y * 90 5A Z + 91 5B [ , 92 5C \ - 93 5D ] . 94 5E ^ / 95 5F _ 0 96 60 ` 1 97 61 a 2 98 62 b 3 99 63 c 4 100 64 d 5 101 65 e 6 102 66 f 7 103 67 g 8 104 68 h 9 105 69 i : 106 6A j 第 368 页 共 928 零死角玩转 STM32—F429 59 3B ; 60 3C < 61 3D = 62 3E > 63 3F ? 64 40 @ 65 41 A 66 42 B 67 43 C 68 44 D 69 45 E 70 46 F 71 47 G 72 48 H 73 49 I 74 4A J 75 4B K 76 4C L 77 4D M 78 4E N 107 6B k 108 6C l 109 6D m 110 6E n 111 6F o 112 70 p 113 71 q 114 72 r 115 73 s 116 74 t 117 75 u 118 76 v 119 77 w 120 78 x 121 79 y 122 7A z 123 7B { 124 7C | 125 7D } 126 7E ~ 79 4F O 127 7F DEL (delete) 删除 后来,计算机引进到其它国家的时候,由于他们使用的不是英语,他们使用的字母在 ASCII 码表中没有定义,所以他们采用 127 号之后的位来表示这些新的字母,还加入了各 种形状,一直编号到 255。从 128 到 255 这些字符被称为 ASCII 扩展字符集。至此基本存 储单位 Byte(char)能表示的编号都被用完了。 28.1.2 中文编码 由于英文书写系统都是由 26 个基本字母组成,利用 26 个字母组可合出不同的单词, 所以用 ASCII 码表就能表达整个英文书写系统。而中文书写系统中的汉字是独立的方块, 若参考单词拆解成字母的表示方式,汉字可以拆解成部首、笔画来表示,但这样会非常复 杂(可参考五笔输入法编码),所以中文编码直接对方块字进行编码,一个汉字使用一个号 码。 由于汉字非常多,常用字就有 6000 多个,如果像 ASCII 编码表那样只使用 1 个字节最 多只能表示 256 个汉字,所以我们使用 2 个字节来编码。 1. GB2312 标准 我们首先定义的是 GB2312 标准。它把 ASCII 码表 127 号之后的扩展字符集直接取消 掉,并规定小于 127 的编码按原来 ASCII 标准解释字符。当 2 个大于 127 的字符连在一起 时,就表示 1 个汉字,第 1 个字节使用 (0xA1-0xFE) 编码,第 2 个字节使用(0xA1-0xFE)编 码,这样的编码组合起来可以表示了 7000 多个符号,其中包含 6763 个汉字。在这些编码 里,我们还把数学符号、罗马字母、日文假名等都编进表中,就连原来在 ASCII 里原本就 第 369 页 共 928 零死角玩转 STM32—F429 有的数字、标点以及字母也重新编了 2 个字节长的编码,这就是平时在输入法里可切换的 “全角”字符,而标准的 ASCII 码表中 127 号以下的就被称为“半角”字符。 表 28-3 说明了 GB2312 是如何兼容 ASCII 码的,当我们设定系统使用 GB2312 标准的 时候,它遇到一个字符串时,会按字节检测字符值的大小,若遇到连续两个字节的数值都 大于 127 时就把这两个连续的字节合在一起,用 GB2312 解码,若遇到的数值小于 127,就 直接用 ASCII 把它解码。 表 28-3 GB2312 兼容 ASCII 码的原理 第 1 字节 0x68 0xB0 区位码 第 2 字节 0x69 0xA1 表示的字符 (hi) (啊) 说明 两个字节的值都小于 127(0x7F),使用 ASCII 解码 两个字节的值都大于 127(0x7F),使用 GB2312 解码 在 GB2312 编码的实际使用中,有时会用到区位码的概念,见图 28-1。GB2312 编码对 所收录字符进行了“分区”处理,共 94 个区,每区含有 94 个位,共 8836 个码位。而区位 码实际是 GB2312 编码的内部形式,它规定对收录的每个字符采用两个字节表示,第一个 字节为“高字节”,对应 94 个区;第二个字节为“低字节”,对应 94 个位。所以它的区 位码范围是:0101-9494。为兼容 ASCII 码,区号和位号分别加上 0xA0 偏移就得到 GB2312 编码。在区位码上加上 0xA0 偏移,可求得 GB2312 编码范围:0xA1A1-0xFEFE, 其中汉字的编码范围为 0xB0A1-0xF7FE,第一字节 0xB0-0xF7(对应区号:16-87),第 二个字节 0xA1-0xFE(对应位号:01-94)。 例如,“啊”字是 GB2312 编码中的第一个汉字,它位于 16 区的 01 位,所以它的区 位码就是 1601,加上 0xA0 偏移,其 GB2312 编码为 0xB0A1。其中区位码为 0101 的码位 表示的是“空格”符。 第 370 页 共 928 零死角玩转 STM32—F429 图 28-1 GB2312 的部分区位码 2. GBK 编码 据统计,GB2312 编码中表示的 6763 个汉字已经覆盖中国大陆 99.75%的使用率,单看 这个数字已经很令人满意了,但是我们不能因为那些文字不常用就不让它进入信息时代了, 而且生僻字在人名、文言文中的出现频率是非常高的。为此我们在 GB2312 标准的基础上 又增加了 14240 个新汉字(包括所有后面介绍的 Big5 中的所有汉字)和符号,这个方案被称 为 GBK 标准。增加了这么多字符,按照 GB2312 原来的格式来编码,2 个字节已经存不下 了,我们聪明的程序员修改了一下格式,不再要求第 2 个字节的编码值必须大于 127,只 要第 1 个字节大于 127 就表示这是一个汉字的开始,这样就做到了兼容 ASCII 和 GB2312 标准了。 表 28-4 说明了 GBK 是如何兼容 ASCII 和 GB2312 标准的,当我们设定系统使用 GBK 标准的时候,它按顺序遍历字符串,按字节检测字符值的大小,若遇到一个字符的值大于 127 时,就再读取它后面的一个字符,把这两个字符值合在一起,用 GBK 解码,解码完后, 再读取第 3 个字符,重新开始以上过程,若该字符值小于 127,则直接用 ASCII 解码。 第 1 字节 0x68(<7F) 0xB0(>7F) 第 2 字节 0xB0(>7F) 0xA1(>7F) 表 28-4 GBK 兼容 ASCII 和 GB2312 的原理 第 节 3字 表示 的字 符 说明 0xA1(>7F) 0x68(<7F) (h 啊) (啊 h) 第 1 个字节小于 127,使用 ASCII 解码,每 2 个字 节大于 127,直接使用 GBK 解码,兼容 GB2312 第 1 个字节大于 127,直接使用 GBK 码解释,第 3 个字节小于 127,使用 ASCII 解码 第 371 页 共 928 零死角玩转 STM32—F429 0xB0(>7F) 0x56(<7F) 0x68(<7F) (癡 h) 第 1 个字节大于 127,第 2 个字节虽然小于 127, 直接使用 GBK 解码,第 3 个字节小于 127,使用 ASCII 解码 3. GB18030 随着计算机技术的普及,我们后来又在 GBK 的标准上不断扩展字符,这些标准被称 为 GB18030,如 GB18030-2000、GB18030-2005 等(“-”号后面的数字是制定标准时的年 号),GB18030 的编码使用 4 个字节,它利用前面标准中的第 2 个字节未使用的“0x30- 0x39”编码表示扩充四字节的后缀,兼容 GBK、GB2312 及 ASCII 标准。 GB18030-2000 主要在 GBK 基础上增加了“CJK(中日韩)统一汉字扩充 A”的汉字。加 上前面 GBK 的内容,GB18030-2000 一共规定了 27533 个汉字(包括部首、部件等)的编 码,还有一些常用非汉字符号。 GB18030-2005 的主要特点是在 GB18030-2000 基础上增加了“CJK(中日韩)统一汉字扩 充 B”的汉字。增加了 42711 个汉字和多种我国少数民族文字的编码(如藏、蒙古、傣、 彝、朝鲜、维吾尔文等)。加上前面 GB18030-2000 的内容,一共收录了 70244 个汉字。 GB2312、GBK 及 GB18030 是汉字的国家标准编码,新版向下兼容旧版,各个标准简 要说明见表 28-5,目前比较流行的是 GBK 编码,因为每个汉字只占用 2 个字节,而且它 编码的字符已经能满足大部分的需求,但国家要求一些产品必须支持 GB18030 标准。 表 28-5 汉字国家标准 类别 编码范围 GB2312 第一字节 0xA1-0xFE 第二字节 0xA1-0xFE GBK 第一字节 0x81-0xFE 第二字节 0x40-0xFE GB1803 0-2000 第一字节 0x81-0xFE 第二字节 0x30-0x39 第三字节 0x81-0xFE 第四字节 0x30-0x39 汉字编码范围 第一字节 0xB0-0xF7 第二字节 0xA1-0xFE 第一字节 0x81-0xA0 第二字节 0x40-0xFE 第一字节 0xAA-0xFE 第二字节 0x40-0xA0 第一字节 0x81-0x82 第二字节 0x30-0x39 第三字节 0x81-0xFE 第四字节 0x30-0x39 扩充汉 字数 6763 6080 8160 6530 说明 除汉字外,还包 括拉丁字母、希 腊字母、日文平 假名及片假名字 母、俄语西里尔 字母在内的 682 个 全角字符 包括部首和构件, 中日韩汉字,包含 了 BIG5 编码中的 所有汉字,加上 GB2312 的 原 内 容,一共有 21003 个汉字 在 GBK 基础上增 加了中日韩统一 汉字扩充 A 的汉 字,加上 GB2312、GBK 的 内容,一共有 27533 个汉字 GB1803 第一字节 0x81-0xFE 0-2005 第二字节 0x30-0x39 第三字节 0x81-0xFE 第一字节 0x95-0x98 第二字节 0x30-0x39 第三字节 0x81-0xFE 42711 在 GB18030-2000 的基础上增加了 42711 中日韩统一 第 372 页 共 928 零死角玩转 STM32—F429 第四字节 0x30-0x39 第四字节 0x30-0x39 汉字扩充 B 中的 汉字和多种我国 少数民族文字的 编码(如藏、蒙 古、傣、彝、朝 鲜、维吾尔文 等),加上前面 GB2312 、 GBK 、 GB18030-2000 的 内容,一共 70244 个汉字 4. Big5 编码 在台湾、香港等地区,使用较多的是 Big5 编码,它的主要特点是收录了繁体字。而从 GBK 编码开始,已经把 Big5 中的所有汉字收录进编码了。即对于汉字部分,GBK 是 Big5 的超集,Big5 能表示的汉字,在 GBK 都能找到那些字相应的编码,但他们的编码是不一 样的,两个标准不兼容,如 GBK 中的“啊”字编码是“0xB0A1”,而 Big5 标准中的编码 为“0xB0DA”。 28.1.3 Unicode 字符集和编码 由于各个国家或地区都根据使用自己的文字系统制定标准,同一个编码在不同的标准 里表示不一样的字符,各个标准互不兼容,而又没有一个标准能够囊括所有的字符,即无 法用一个标准表达所有字符。国际标准化组织(ISO)为解决这一问题,它舍弃了地区性的方 案,重新给全球上所有文化使用的字母和符号进行编号,对每个字符指定一个唯一的编号 (ASCII 中原有的字符编号不变),这些字符的号码从 0x000000 到 0x10FFFF,该编号集被称 为 Universal Multiple-Octet Coded Character Set,简称 UCS,也被称为 Unicode。最新版的 Unicode 标准还包含了表情符号(聊天软件中的部分 emoji 表情),可访问 Unicode 官网了解: http://www.unicode.org。 Unicode 字符集只是对字符进行编号,但具体怎么对每个字符进行编码,Unicode 并没 指定,因此也衍生出了如下几种 unicode 编码方案(Unicode Transformation Format)。 28.1.4 UTF-32 对 Unicode 字符集编码,最自然的就是 UTF-32 方式了。编码时,它直接对 Unicode 字符集里的每个字符都用 4 字节来表示,转换方式很简单,直接将字符对应的编号 数字转换为 4 字节的二进制数。如表 28-6,由于 UTF-32 把每个字符都用要 4 字节来存储, 因此 UTF-32 不兼容 ASCII 编码,也就是说 ASCII 编码的文件用 UTF-32 标准来打开会成为 乱码。 字符 A 啊 表 28-6 UTF-32 编码示例 GBK 编码 Unicode 编号 UTF-32 编码 0x41 0x0000 0041 大端格式 0x0000 0041 0xB0A1 0x0000 554A 大端格式 0x0000 554A 第 373 页 共 928 零死角玩转 STM32—F429 对 UTF-32 数据进行解码的时候,以 4 个字节为单位进行解析即可,根据编码可直接 找到 Unicode 字符集中对应编号的字符。 UTF-32 的优点是编码简单,解码也很方便,读取编码的时候每次都直接读 4 个字节, 不需要加其它的判断。它的缺点是浪费存储空间,大量常用字符的编号只需要 2 个字节就 能表示。其次,在存储的时候需要指定字节顺序,是高位字节存储在前(大端格式),还是 低位字节存储在前(小端格式)。 28.1.5 UTF-16 针对 UTF-32 的缺点,人们改进出了 UTF-16 的编码方式,它采用 2 字节或 4 字节的变 长编码方式(UTF-32 定长为 4 字节)。对 Unicode 字符编号在 0 到 65535 的统一用 2 个字节 来表示,将每个字符的编号转换为 2 字节的二进制数,即从 0x0000 到 0xFFFF。而由于 Unicode 字符集在 0xD800-0xDBFF 这个区间是没有表示任何字符的,所以 UTF-16 就利用 这段空间,对 Unicode 中编号超出 0xFFFF 的字符,利用它们的编号做某种运算与该空间建 立映射关系,从而利用该空间表示 4 字节扩展,感兴趣的读者可查阅相关资料了解具体的 映射过程。 表 28-7 UTF-16 编码示例 字符 A 啊 GB18030 编码 0x41 0xB0A1 0x9735 F832 Unicode 编号 0x0000 0041 0x0000 554A 0x0002 75CC UTF-16 编码 大端格式 0x0041 大端格式 0x554A 大端格式 0xD85D DDCC 注: 五笔:TLHH(不支持 GB18030 码的输入法无法找到该字,感兴趣可搜索它的 Unicode 编号找到) UTF-16 解码时,按两个字节去读取,如果这两个字节不在 0xD800 到 0xDFFF 范围内, 那就是双字节编码的字符,以双字节进行解析,找到对应编号的字符。如果这两个字节在 0xD800 到 0xDFFF 之间,那它就是四字节编码的字符,以四字节进行解析,找到对应编号 的字符。 UTF-16 编码的优点是相对 UTF-32 节约了存储空间,缺点是仍不兼容 ASCII 码,仍有 大小端格式问题。 28.1.6 UTF-8 UTF-8 是目前 Unicode 字符集中使用得最广的编码方式,目前大部分网页文件已使用 UTF-8 编码,如使用浏览器查看百度首页源文件,可以在前几行 HTML 代码中找到如下代 码: 1 其中“charset”等号后面的“utf-8”即表示该网页字符的编码方式 UTF-8。 UTF-8 也是一种变长的编码方式,它的编码有 1、2、3、4 字节长度的方式,每个 Unicode 字符根据自己的编号范围去进行对应的编码,见表 28-8。它的编码符合以下规律:  对于 UTF-8 单字节的编码,该字节的第 1 位设为 0(从左边数起第 1 位,即最高位), 剩余的位用来写入字符的 Unicode 编号。即对于 Unicode 编号从 0x0000 00000x0000 007F 的字符,UTF-8 编码只需要 1 个字节,因为这个范围 Unicode 编号的 字符与 ASCII 码完全相同,所以 UTF-8 兼容了 ASCII 码表。 第 374 页 共 928 零死角玩转 STM32—F429  对于 UTF-8 使用 N 个字节的编码(N>1),第一个字节的前 N 位设为 1,第 N+1 位 设为 0,后面字节的前两位都设为 10,这 N 个字节的其余空位填充该字符的 Unicode 编号,高位用 0 补足。 表 28-8 UTF-8 编码原理(x 的位置用于填充 Unicode 编号) Unicode(16 进制) UTF-8(2 进制) 编号范围 第一字节 第二字节 第三字节 第四字节 第五字节 00000000-0000007F 00000080-000007FF 00000800-0000FFFF 00010000-0010FFFF … 0xxxxxxx 110xxxxx 1110xxxx 11110xxx 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 注:实际上 utf-8 编码长度最大为四个字节,所以最多只能表示 Unicode 编码值的二进制数为 21 位的 Unicode 字 符。但是已经能表示所有的 Unicode 字符,因为 Unicode 的最大码位 0x10FFFF 也只有 21 位。 UTF-8 解码的时候以字节为单位去看,如果第一个字节的 bit 位以 0 开头,那就是 ASCII 字符,以单字节进行解析。如果第一个字节的数据位以“110”开头,就按双字节进 行解析,3、4 字节的解析方法类似。 UTF-8 的优点是兼容了 ASCII 码,节约空间,且没有字节顺序的问题,它直接根据第 1 个字节前面数据位中连续的 1 个数决定后面有多少个字节。不过使用 UTF-8 编码汉字平 均需要 3 个字节,比 GBK 编码要多一个字节。 28.1.7 BOM 由于 UTF 系列有多种编码方式,而且对于 UTF-16 和 UTF-32 还有大小端的区分,那 么计算机软件在打开文档的时候到底应该用什么编码方式去解码呢?有的人就想到在文档 最前面加标记,一种标记对应一种编码方式,这些标记就叫做 BOM(Byte Order Mark),它 们位于文本文件的开头,见表 28-9。注意 BOM 是对 Unicode 的几种编码而言的,ANSI 编 码没有 BOM。 表 28-9 BOM 标记 BOM 标记 表示的编码 0xEF 0xBB 0xBF 0xFF 0xFE 0xFE 0xFF UTF-8 UTF-16 小端格式 UTF-16 大端格式 0xFF 0xFE 0x00 0x00 UTF-32 小端格式 0x00 0x00 0xFE 0xFF UTF-32 大端格式 但由于带 BOM 的设计很多规范不兼容,不能跨平台,所以这种带 BOM 的设计没有流 行起来。Linux 系统下默认不带 BOM。 28.2 什么是字模? 有了编码,我们就能在计算机中处理、存储字符了,但是如果计算机处理完字符后直 接以编码的形式输出,人类将难以识别。来,在 2 秒内告诉我 ASCII 编码的“0x25”表示 什么字符?不容易吧?要是觉得容易,再来告诉我 GBK 编码的“0xBCC6”表示什么字符。 因此计算机与人交互时,一般会把字符转化成人类习惯的表现形式进行输出,如显示、打 印的时候。 第 375 页 共 928 零死角玩转 STM32—F429 但是如果仅有字符编码,计算机还不知道该如何表达该字符,因为字符实际上是一个 个独特的图形,计算机必须把字符编码转化成对应的字符图形人类才能正常识别,因此我 们要给计算机提供字符的图形数据,这些数据就是字模,多个字模数据组成的文件也被称 为字库。计算机显示字符时,根据字符编码与字模数据的映射关系找到它相应的字模数据, 液晶屏根据字模数据显示该字符。 28.2.1 字模的构成 已知字模是图形数据,而图形在计算机中是由一个个像素点组成的,所以字模实质是 一个个像素点数据。为方便处理,我们把字模定义成方块形的像素点阵,且每个像素点只 有 0 和 1 这两种状态(可以理解为单色图像数据)。见图 28-2,这是两个宽、高为 16x16 的 像素点阵组成的两个汉字图形,其中的黑色像素点即为文字的笔迹。计算机要表示这样的 图形,只需使用 16x16 个二进制数据位,每个数据位记录一个像素点的状态,把黑色像素 点以“1”表示,无色像素点以“0”表示即可。这样的一个汉字图形,使用 16x16/8=32 个 字节来就可以记录下来。 Bit15 ~Bit0 Bit15 ~Bit0 Bit0~Bit15 每个字的数 据量: 16x16/8 (字节) 图 28-2 字模 16x16 的“字”的字模数据以 C 语言数组的方式表示,见代码清单 28-1。在这样的字 模中,以两个字节表示一行像素点,16 行构成一个字模。 代码清单 28-1:“字”的字模数据 1. /* 字 */ 2. unsigned char code Bmp003[]= 3. { 4. /*-----------------------------------------------------------5. ; 源文件 / 文字 : 字 6. ; 宽×高(像素): 16×16 7. ; 字模格式/大小 : 单色点阵液晶字模,横向取模,字节正序/32 字节 8. ----------------------------------------------------------*/ 9. 10. 0x02,0x00,0x01,0x00,0x3F,0xFC,0x20,0x04,0x40,0x08,0x1F,0xE0,0x00,0x40, 0x00,0x80, 11. 0xFF,0xFF,0x7F,0xFE,0x01,0x00,0x01,0x00,0x01,0x00,0x01,0x00,0x05,0x00, 0x02,0x00, 12. }; 第 376 页 共 928 零死角玩转 STM32—F429 28.2.2 字模显示原理 如果使用 LCD 的画点函数,按位来扫描这些字模数据,把为 1 的位以黑色来显示(也 可以使用其它颜色),为 0 的数据位以白色来显示,即可把整个点阵还原出来,显示在液晶 屏上。 为便于理解,我们编写了一个使用串口 printf 利用字模打印字符到串口上位机,见代 码清单 28-2 中演示的字模显示原理。代码清单 28-1 代码清单 28-2 使用串口利用字模打印字到上位机 1 2 /*"当"字符的字模*/ 3 unsigned char charater_matrix[] = 4 { 5 0x00,0x80,0x10,0x90,0x08,0x98,0x0C,0x90, 6 0x08,0xA0,0x00,0x80,0x3F,0xFC,0x00,0x04, 7 0x00,0x04,0x1F,0xFC,0x00,0x04,0x00,0x04, 8 0x00,0x04,0x3F,0xFC,0x00,0x04,0x00,0x00 9 }; 10 11 /** 12 * @brief 使用串口在上位机打印字模 13 * 演示字模显示原理 14 * @retval 无 15 */ 16 void Printf_Charater(void) 17 { 18 int i,j; 19 unsigned char kk; 20 21 /*i 用作行计数*/ 22 for ( i=0; i<16; i++) 23 { 24 /*j 用作一字节内数据的移位计数*/ 25 /*一行像素的第一个字节*/ 26 for (j=0; j<8; j++) 27 { 28 /*一个数据位一个数据位地处理*/ 29 kk = charater_matrix[2*i] << j ; //左移 J 位 30 if ( kk & 0x80) 31 { 32 printf("*"); //如果最高位为 1,输出"*"号,表示笔迹 33 } 34 else 35 { 36 printf(" "); //如果最高位为 0,输出"空格",表示空白 37 } 38 } 39 /*一行像素的第二个字节*/ 40 for (j=0; j<8; j++) 41 { 42 kk = charater_matrix[2*i+1] << j ; //左移 J 位 43 44 if ( kk & 0x80) 45 { 46 printf("*"); //如果最高位为 1,输出"*"号,表示笔迹 47 } 48 else 49 { 50 printf(" "); //如果最高位为 0,输出"空格",表示空白 第 377 页 共 928 零死角玩转 STM32—F429 51 } 52 } 53 printf("\n"); //输出完一行像素,换行 54 } 55 printf("\n\n"); //一个字输出完毕 56 } 在 main 函数中运行这段代码,连接好开发板到上位机,可以看到图 28-3 中的现象。 该函数中利用 printf 函数对字模数据中为 1 的数据位打印“*”号,为 0 的数据位打印出 “空格”,从而在串口接收区域中使用“*”号表达出了一个“当”字。 28.2.3 如何制作字模 图 28-3 使用串口打印字模 以上只是某几个字符的字模,为方便使用,我们需要制作所有常用字符的字模,如程 序只需要英文显示,那就需要制作包含 ASCII 码表 28-2 中所有字符的字模,如程序只需要 使用一些常用汉字,我们可以选择制作 GB2312 编码里所有字符的字模,而且我们希望字 模数据与字符编码有固定的映射关系,以便我们在程序中使用字符编码作为索引,查找字 模。在网上搜索可找到一些制作字模的软件工具,可满足这些需求。在我们提供的 《LTDC—液晶显示汉字》的工程目录下提供了一个取模软件“PCtoLCD”,这里以它为 例讲解如何制作字模,其它字模软件也是类似的。 (1) 配置字模格式 打开取模软件,点击“选项”菜单,会弹出一个对话框,见图 28-4。  选项“点阵格式”中的阴、阳码是指字模点阵中有笔迹像素位的状态是“1”还是 “0”,像我们前文介绍的那种就是阴码,反过来就是阳码。本工程中使用阴码。  选项“取模方式”是指字模图形的扫描方向,修改这部分的设置后,选项框的右 侧会有相应的说明及动画显示,这里我们依然按前文介绍的字模类型,把它配置 成“逐行式”  选项“每行显示的数据”里我们把点阵和索引都配置成 24,设置这个点阵的像素 大小为 24x24。 第 378 页 共 928 零死角玩转 STM32—F429 字模选项的格式保持不变,设置完我们点击确定即可,字模选项的这些配置会影响到 显示代码的编写方式,即类似前文代码清单 28-2 中的程序。 图 28-4 配置字模格式 (2) 生成 GB2312 字模 配置完字模选项后,点击软件中的导入文本图标,会弹出一个“生成字库”的对话框, 点击右下角的生成国标汉字库按钮即可生成包含了 GB2312 编码里所有字符的字模文件。 在《LTDC—液晶显示汉字》的工程目录下的《GB2312_H2424.FON》是我用这个取模软件 生成的字模原文件,若不想自己制作字模,可直接使用该文件。 第 379 页 共 928 零死角玩转 STM32—F429 28.2.4 字模寻址公式 图 28-5 生成国标汉字库 使用字模软件制作的字模数据一般会按照编码格式排列。如我们利用以上软件生成的 字模文件《GB2312_H2424.FON》中的数据,是根据 GB2312 的区位码表的顺序存储的, 它存储了区位码为 0101-9494 的字符,每个字模的大小为 24x24/8=72 字节。其中第一个字 符“空格”的区位码为 0101,它是首个字符,所以文件的前 72 字节存储的是它的字模数 据;同理,72-144 字节存储的则是 0102 字符“、”的字模数据。所以我们可以导出任意字 符的寻址公式: Addr = (((CodeH-0xA0-1)*94) +(CodeL-0xA0-1))*24*24/8 其中 CodeH 和 CodeL 分别是 GB2312 编码的第一字节和第二字节;94 是指一个区中有 94 个位(即 94 个字符)。公式的实质是根据字符的 GB2312 编码,求出区位码,然后区位码 乘以每个字符占据的字节数,求出地址偏移。 28.2.5 存储字模文件 上面生成的《GB2312_H2424.FON》文件的大小为 576KB,比很多 STM32 芯片内部的 所有 FLASH 空间都大,如果我们还是在程序中直接以 C 语言数组的方式存储字模数据, STM32 芯片的程序空间会非常紧张,一般的做法是把字模数据存储到外部存储器,如 SD 卡或 SPI-FLASH 芯片,当需要显示某个字符时,控制器根据字符的编码算好字模的存储地 址,再从存储器中读取,而 FLASH 芯片在生产前就固化好字模内容,然后直接把 FLASH 芯片贴到电路板上,作为整个系统的一部分。 第 380 页 共 928 零死角玩转 STM32—F429 28.3 LTDC—各种模式的液晶显示字符实验 本小节讲解如何利用字模使用在液晶屏上显示字符。 根据编码或字模存储位置、使用方式的不同,讲解中涉及到多个工程,见表 28-10 中 的说明,在讲解特定实验的时候,请读者打开相应的工程来阅读 表 28-10 各种模式的液晶显示字符实验 工程名称 说明 LTDC — 液 晶 显 示 英 文 ( 字 库 在 内 部 FLASH) LTDC — 液 晶 显 示 汉 字 ( 字 库 在 外 部 FLASH) 仅包含 ASCII 码字符显示功能,字库直接以 C 语言常量数组的方式存储在 STM32 芯片的内 部 FLASH 空间 包含 ASCII 码字符及 GB2312 码字符的显示功 能 , ASCII 码 字 符 存 储 在 STM32 内 部 FLASH , GB2312 码 字 符 存 储 在 外 部 SPIFLASH 芯片 LTDC—LCD 显示汉字(字库在 SD 卡) LTDC—液晶显示汉字(显示任意大小) 包含 ASCII 码字符及 GB2312 码字符的显示功 能 , ASCII 码 字 符 存 储 在 STM32 内 部 FLASH,GB2312 码字符直接以文件的格式存 储在 SD 卡中 在基础字库的支持下,使用字库缩放函数,使 得只用一种字库,就能显示任意大小的字符。 包含 ASCII 码字符及 GB2312 码字符的显示功 能 , ASCII 码 字 符 存 储 在 STM32 内 部 FLASH , GB2312 码 字 符 存 储 在 外 部 SPIFLASH 芯片 这些实验是在“LTDC/DMA2D—液晶显示”工程的基础上修改的,主要添加了字符显 示相关的内容,本小节只讲解这部分新增的函数。关于液晶驱动的原理在此不再赘述,不 理解这部分的可阅读前面的章节。 28.3.1 硬件设计 针对不同模式的液晶显示字符工程,需要有不同的硬件支持。字库存储在 STM32 芯片 内部 FLASH 的工程,只需要液晶屏和 SDRAM 的支持即可,跟普通液晶显示的硬件需求 无异。需要外部字库的工程,要有额外的 SPI-FLASH、SD 支持,使用外部 FLASH 时,我 们的实验板上直接用板子上的 SPI-FLASH 芯片存储字库,出厂前我们已给 FLASH 芯片烧 录了前面的《GB2312_H2424.FON》字库文件,如果您想把我们的程序移植到您自己设计 产品上,请确保该系统包含有存储了字库的 FLASH 芯片,才能正常显示汉字使用 SD 卡时, 需要给板子接入存储有《GB2312_H2424.FON》字库文件的 MicroSD 卡,SD 卡的文件系统 格式需要是 FAT 格式,且字库文件所在的目录需要跟程序里使用的文件目录一致。 关于 SPI-FLASH 和 SD 卡的原理图及驱动说明可参考其他的章节。给外部 SPI-FLASH 和 SD 卡存储字库的操作我们将在另一个文档中说明,本章的教程默认您已配置好 SDIFLASH 和 SD 卡相关的字库环境。 第 381 页 共 928 零死角玩转 STM32—F429 28.3.2 显示 ASCII 编码的字符 我们先来看如何显示 ASCII 码表中的字符,请打开“LTDC—液晶显示英文(字库在 内部 FLASH)”的工程文件。本工程中我们把字库数据相关的函数代码写在“fonts.c”及 “fonts.h”文件中,字符显示的函数仍存储在 LCD 驱动文件“bsp_lcd.c”及“bsp_lcd.h” 中。 1. 编程要点 (25) 获取字模数据; (26) 根据字模格式,编写液晶显示函数; (27) 编写测试程序,控制液晶英文。 2. 代码分析 ASCII 字模数据 要显示字符首先要有字库数据,在工程的“fonts.c”文件中我们定义了一系列大小为 16x24、12x12、8x12 及 8x8 的 ASCII 码表的字模数据,其形式见代码清单 24-2。 代码清单 28-3 部分英文字库 16x24 大小(fonts.c 文件) 1 /** @defgroup FONTS_Private_Variables 2 * @{ 3 */ 4 const uint16_t ASCII16x24_Table [] = 5{ 6 /** 7 * @brief Space ' ' 8 */ 9 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 10 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 11 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 12 /** 13 * @brief '!' 14 */ 15 0x0000, 0x0180, 0x0180, 0x0180, 0x0180, 0x0180, 0x0180, 0x0180, 16 0x0180, 0x0180, 0x0180, 0x0180, 0x0180, 0x0180, 0x0000, 0x0000, 17 0x0180, 0x0180, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 18 /** 19 * @brief '"' 20 */ 21 0x0000, 0x0000, 0x00CC, 0x00CC, 0x00CC, 0x00CC, 0x00CC, 0x00CC, 22 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 23 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 24 /** 25 * @brief '#' 26 */ 27 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0C60, 0x0C60, 28 0x0C60, 0x0630, 0x0630, 0x1FFE, 0x1FFE, 0x0630, 0x0738, 0x0318, 29 0x1FFE, 0x1FFE, 0x0318, 0x0318, 0x018C, 0x018C, 0x018C, 0x0000, 30 /*以下部分省略.....*/ 由于 ASCII 中的字符并不多,所以本工程中直接以 C 语言数组的方式存储这些字模数 据,C 语言的 const 数组是作为常量直接存储到 STM32 芯片的内部 FLASH 中的,所以如 第 382 页 共 928 零死角玩转 STM32—F429 果您不需要显示中文,可以不用外部的 SPI-FLASH 芯片,可省去烧录字库的麻烦。以上代 码定义的 ASCII16x24_Table 数组是 16x24 大小的 ASCII 字库。 管理英文字模的结构体 为了方便使用各种不同的字体,工程中定义了一个“_tFont”结构体类型,并利用它 定义存储了不同字体信息的结构体变量,见代码清单 24-3。 代码清单 28-4 管理英文字模的结构体(fonts.c 文件) 1 /*字体格式*/ 2 typedef struct _tFont 3{ 4 const uint16_t *table; /*指向字模数据的指针*/ 5 uint16_t Width; /*字模的像素宽度*/ 6 uint16_t Height; /*字模的像素高度*/ 7 } sFONT; 8 9 sFONT Font16x24 = 10 { 11 ASCII16x24_Table, /*指向 16x24 的字模数组*/ 12 16, /* Width */ 13 24, /* Height */ 14 }; 15 16 sFONT Font12x12 = 17 { 18 ASCII12x12_Table,/*指向 12x12 的字模数组*/ 19 12, /* Width */ 20 12, /* Height */ 21 }; 22 23 sFONT Font8x12 = 24 { 25 ASCII8x12_Table, 26 8, /* Width */ 27 12, /* Height */ 28 }; 29 30 sFONT Font8x8 = 31 { 32 ASCII8x8_Table, 33 8, /* Width */ 34 8, /* Height */ 35 }; 这个结构体类型定义了三个变量,第一个是指向字模数据的指针,即前面提到的 C 语 言数组,每二、三个变量存储了该字模单个字符的像素宽度和高度。利用这个类型定义了 Font16x24、Font12x12 之类的变量,方便显示时寻址。 切换字体 在程序中若要方便切换字体,还需要定义一个存储了当前选择字体的变量 LCD_Currentfonts,代码清单 24-4。 代码清单 28-5 切换字体(bsp_lcd.c 文件) 1 /*用于存储当前选择的字体格式的全局变量*/ 2 static sFONT *LCD_Currentfonts; 3 /** 4 * @brief 设置字体格式(英文) 5 * @param fonts: 选择要设置的字体格式 6 * @retval None 第 383 页 共 928 零死角玩转 STM32—F429 7 */ 8 void LCD_SetFont(sFONT *fonts) 9{ 10 LCD_Currentfonts = fonts; 11 } 使用 LCD_SetFont 可以切换 LCD_Currentfonts 指向的字体类型,函数的可输入参数即 前面的 Font16x24、Font12x12 之类的变量。 ASCII 字符显示函数 利用字模数据以及上述结构体变量,我们可以编写一个能显示各种字体的通用函数, 见代码清单 44-7。 代码清单 28-6 ASCII 字符显示函数 1 /** 2 * @brief 显示一个字符到液晶屏. 3 * @param Xpos: 字符要显示到的液晶行地址. 4 * @param Ypos: 字符要显示到的液晶列地址 5 * @param c: 指针,指向要显示字符的字模数据的地址 6 * @retval None 7 */ 8 void LCD_DrawChar(uint16_t Xpos, uint16_t Ypos, const uint16_t *c) 9{ 10 uint32_t index = 0, counter = 0, xpos =0; 11 uint32_t Xaddress = 0; 12 13 /*xpos 表示当前行的显存偏移位置*/ 14 xpos = Xpos*LCD_PIXEL_WIDTH*3; 15 /*Xaddress 表示液晶像素点所在液晶屏的列位置*/ 16 Xaddress = Ypos; 17 18 /*index 用于行计数*/ 19 for (index = 0; index < LCD_Currentfonts->Height; index++) 20 { 21 /*counter 用于行内像素点的位置计数*/ 22 for (counter = 0; counter < LCD_Currentfonts->Width; counter++) 23 { 24 /*根据字模数据判断是有色像素还是无色像素*/ 25 if ((((c[index] & ((0x80 << ((LCD_Currentfonts->Width / 12 ) * 8 )) >> counter)) == 0x00) && 26 (LCD_Currentfonts->Width <= 12))|| 27 (((c[index] & (0x1 << counter)) == 0x00)&&(LCD_Currentfonts->Width > 12 ))) 28 { 29 /*显示背景色*/ 30 *(__IO uint16_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos) = 31 (0x00FFFF & CurrentBackColor); //GB 32 *(__IO uint8_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos+2) = 33 (0xFF0000 & CurrentBackColor) >> 16; //R 34 } 35 else 36 { 37 /*显示字体颜色*/ 38 *(__IO uint16_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos) = 39 (0x00FFFF & CurrentTextColor); //GB 40 *(__IO uint8_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos+2) = 第 384 页 共 928 零死角玩转 STM32—F429 41 (0xFF0000 & CurrentTextColor) >> 16; //R 42 } 43 /*指向当前行的下一个点*/ 44 Xaddress++; 45 } 46 /*显示完一行*/ 47 /*指向字符显示矩阵下一行的第一个像素点*/ 48 Xaddress += (LCD_PIXEL_WIDTH - LCD_Currentfonts->Width); 49 } 50 } 这个函数与前文中的串口打印字模到上位机的那个函数原理是一样的,只是这个函数 要使用液晶显示,所以需要计算显存地址,在特定的显存地址写入数据。为方便理解,请 配合表 28-11 理解,该表代表液晶面板的部分像素,每个单元格表示一个液晶像素点,其 中绿色部分代表上述函数操作涉及的字符显示矩阵,黄色代表其余无关像素点。 表 28-11 液晶显示字符说明表 Ypos Xpos      该函数的说明如下: (1) 输入参数 这个字符显示函数有 Xpos、Ypos 及 c 参数。其中 Xpos 和 Ypos 分别表示字符显示位 置的像素行号及像素列号,如表中的 Xpos 和 Ypos 参数确定了字符要显示的像素位置, 即标号“”处。而输入参数 c 是一个指向将要显示的字符的字模数据的指针,它的 指针地址由上层函数计算,在本函数中我们只需要知道它是指向某个字模的数据即可, 我们将要利用它的数据,填充表中的绿色单元格,即字符显示矩阵。 (2) xpos 与 Xaddress 变量 第 385 页 共 928 零死角玩转 STM32—F429 输入参数经过处理,被存储在 xpos 及 Xaddress 变量中,注意这个“xpos”是小写的, 与输入参数“Xpos”不一样。xpos 中存储的是标号“” 处像素点的显存地址,它由 输入参数“Xpos*液晶每行像素个数*每个像素字节数”得到。而 Xaddress 在函数刚开 始的时候直接让它等于输入参数“Ypos”,它表示与标号“”处像素点的偏移,单 位为像素点个数,刚开始的时候它的偏移指向标号“”处的像素点。 (3) 行循环与列循环 计算好地址后,就可以根据字模数据处理了,函数使用了两个 for 循环,对字符显示 矩阵里每个像素位进行遍历,一个点一点点地描上颜色。其中内层 for 循环用于遍历 行内像素位,外层 for 循环用于遍历像素行。for 循环中的判断条件利用了当前选择字 体 LCD_Currentfonts 的宽度及高度变量,以使函数适应不同的字模格式。 (4) 判断像素位的状态 在 for 循环里遍历每个像素位时,有一个 if 条件判断,它根据字模数据中的数据位决 定特定像素要显示字体颜色还是背景颜色。代码中的判断条件加入了字体的宽度变量 进行运算,对不同字模数据进行不同的处理。由于 ASCII 码表的这部分字模是 ST 官 方例程给出的,我们不清楚这些字模数据是如何存储,所以也很难分析这段代码中的 判断代码具体为何要这样写,只要知道在这个判断条件后,程序分出了当前遍历到的 像素点该显示背景颜色还是显示字体颜色这两个分支即可。 (5) 设置像素点的颜色 经过判断分支后,字符显示矩阵对于字模数据位中的两种状态分别显示背景色 CurrentBackColor 和字体颜色 CurrentTextColor。这部分实质上跟液晶屏绘制像素点的 原理是一样的,想控制某像素点的颜色,就往该像素点的显存空间写入颜色数据即可, 代码中利用(CurrentFrameBuffer + (3*Xaddress) + xpos)表示当前像素点的显存空间首地 址,其中 CurrentFrameBuffer 是当前液晶层的显存首地址,它加上 xpos 的行地址偏移, 再加上“每个像素点的字节数*Xaddress”就可以计算出当前要操作的像素点地址了。 (6) 处理其它像素点 处理完第一个像素点后,Xaddress 变量自加 1,指向标号“”中的像素点,循环此 过种,一直到处理完当前字符显示矩阵的一整行像素点,退出内循环然后对 Xaddress 变量作行偏移运算:Xaddress += (LCD_PIXEL_WIDTH - LCD_Currentfonts->Width), 使 Xaddress 指向字符显示矩阵下一行的首个像素点,即标号“”处的像素点,运算 中加入的偏移量就是标号“”与标号“”之间的像素点个数(表格中的紫色区域)。 执行完整个外层 for 循环,就把一个字符的所有像素点描绘完了。 直接使用 ASCII 码显示字符 上面的函数需要直接指定要显示的字符的字模地址,不符合使用习惯,为此我们再定 义一个函数 LCD_DisplayChar,使得可以直接用 ASCII 字符串来显示,见代码清单 27-8。 代码清单 28-7 直接使用 ASCII 码显示字符 1 /** @defgroup FONTS_Exported_Constants 2 * @{ 3 */ 4 #define LINE(x) ((x) * (((sFONT *)LCD_GetFont())->Height)) 5 第 386 页 共 928 零死角玩转 STM32—F429 6 /** 7 * @brief 显示一个字符(英文). 8 * @param Line: 要显示的行编号 LINE(0) - LINE(N) 9 * @param Column: 字符所在的液晶列地址 10 * @param Ascii: ASCII 字符编码 0x20 and 0x7E 11 * 调用格式:LCD_DisplayChar(LINE(1),48,'A') 12 * @retval None 13 */ 14 void LCD_DisplayChar(uint16_t Line, uint16_t Column, uint8_t Ascii) 15 { 16 Ascii -= 32; 17 18 LCD_DrawChar(Line, Column, 19 &LCD_Currentfonts->table[Ascii * LCD_Currentfonts- >Height]); 20 } 该函数利用包含 Line,Column 及 Ascii 三个输入参数。其中 Line 参数可以输入宏 “LINE(x)”其中 x 表示字符要显示在液晶屏的哪一行,利用这个宏可以把液晶屏显示区域 按照字符高度来分行,方便显示操作。Column 参数用于控制字符显示在液晶屏上的哪一列。 Ascii 参数用于输入要显示字符的 ASCII 编码,在程序中我们可以使用“ ‗A‘ ”这种形式赋 值。 显示字符串 继续对以上函数进行封装,我们可以得到 ASCII 字符的字符串显示函数,见代码清单 27-9。 代码清单 28-8 字符串显示函数 1 /** 2 * @brief 显示一行字符(英文),若超出液晶宽度,不自动换行。 3 * @param Line: 要显示的行编号 LINE(0) - LINE(N) 4 * @param *ptr: 要显示的字符串指针 5 * 调用格式:LCD_DisplayStringLine(LINE(1),”test”) 6 */ 7 void LCD_DisplayStringLine(uint16_t Line, uint8_t *ptr) 8{ 9 uint16_t refcolumn = 0; 10 /* 判断显示位置不能超出液晶的边界 */ 11 while ((refcolumn < LCD_PIXEL_WIDTH) && 12 ((*ptr != 0) & (((refcolumn + LCD_Currentfonts->Width) & 0xFFFF) >= 13 LCD_Currentfonts->Width))) 14 { 15 /* 使用 LCD 显示一个字符 */ 16 LCD_DisplayChar(Line, refcolumn, *ptr); 17 /* 根据字体偏移显示的位置 */ 18 refcolumn += LCD_Currentfonts->Width; 19 /* 指向字符串中的下一个字符 */ 20 ptr++; 21 } 22 } 本函数中的输入参数 ptr 为指向要显示的字符串的指针,在函数的内部它把字符串中 的字符一个个地利用 LCD_DisplayChar 函数显示到液晶屏上。使用这个函数,我们可以很 方便地利用“LCD_DisplayStringLine(LINE(1),‖test‖)”这样的格式在液晶屏上直接显示一串 字符。 第 387 页 共 928 零死角玩转 STM32—F429 显示 ASCII 码示例 下面我们再来看 main 文件是如何利用这些函数显示 ASCII 码字符的,见代码清单 24-14。 代码清单 28-9 显示 ASCII 码的 main 函数 1 /** 2 * @brief 主函数 3 * @param 无 4 * @retval 无 5 */ 6 int main(void) 7{ 8 /* LED 端口初始化 */ 9 LED_GPIO_Config(); 10 11 /*初始化液晶屏*/ 12 LCD_Init(); 13 LCD_LayerInit(); 14 LTDC_Cmd(ENABLE); 15 16 /*把背景层刷黑色*/ 17 LCD_SetLayer(LCD_BACKGROUND_LAYER); 18 LCD_Clear(LCD_COLOR_BLACK); 19 20 /*初始化后默认使用前景层*/ 21 LCD_SetLayer(LCD_FOREGROUND_LAYER); 22 /*默认设置不透明 ,该函数参数为不透明度,范围 0-0xff ,0 为全透明,0xff 为不透明 */ 23 LCD_SetTransparency(0xFF); 24 LCD_Clear(LCD_COLOR_BLACK); 25 /*经过 LCD_SetLayer(LCD_FOREGROUND_LAYER)函数后, 26 以下液晶操作都在前景层刷新,除非重新调用过 LCD_SetLayer 函数设置背景层*/ 27 28 LED_BLUE; 29 Delay(0xfff); 30 31 while (1) { 32 LCD_Test(); 33 } 34 } main 函数中主要是对液晶屏初始化,初始化完成后就能够显示 ASCII 码字符了,无需 利用 SPI-FLASH 及 SD 卡。在 while 循环中调用的 LCD_Test 函数包含了显示字符的函数调 用示例,见代码清单 28-10。 代码清单 28-10 LCD_Test 函数中的 ASCII 码显示示例 1 /*用于测试各种液晶的函数*/ 2 void LCD_Test(void) 3{ 4 /*演示显示变量*/ 5 static uint8_t testCNT = 0; 6 char dispBuff[100]; 7 8 testCNT++; 9 10 /*使用不透明前景层*/ 11 LCD_SetLayer(LCD_FOREGROUND_LAYER); 12 LCD_SetTransparency(0xff); 13 14 LCD_Clear(LCD_COLOR_BLACK); /* 清屏,显示全黑 */ 第 388 页 共 928 零死角玩转 STM32—F429 15 16 /*设置字体颜色及字体的背景颜色(此处的背景不是指 LCD 的背景层!注意区分)*/ 17 LCD_SetColors(LCD_COLOR_WHITE,LCD_COLOR_BLACK); 18 19 /*选择字体*/ 20 LCD_SetFont(&Font16x24); 21 LCD_DisplayStringLine(LINE(1),(uint8_t* )"BH 5.0 inch LCD para:"); 22 LCD_DisplayStringLine(LINE(2),(uint8_t* )"Image resolution:800x480 px"); 23 LCD_DisplayStringLine(LINE(3),(uint8_t* )"Touch pad:5 point touch 24 supported"); 25 LCD_DisplayStringLine(LINE(4),(uint8_t* )"Use STM32-LTDC directed 26 driver,"); 27 LCD_DisplayStringLine(LINE(5),(uint8_t* )"no extern lcd driver 28 needed,RGB888,24bits data bus"); 29 LCD_DisplayStringLine(LINE(6),(uint8_t* )"Touch pad use IIC to 30 communicate"); 31 32 /*使用 c 标准库把变量转化成字符串*/ 33 sprintf(dispBuff,"Display value demo: testCount = %d ",testCNT); 34 LCD_ClearLine(LINE(7)); 35 36 /*然后显示该字符串即可,其它变量也是这样处理*/ 37 LCD_DisplayStringLine(LINE(7),(uint8_t* )dispBuff); 38 /*... 以下省略其它液晶测试函数的内容*/ 39 } 40 这段代码包含了使用字符串显示函数显示常量字符和变量的示例。显示常量字符串时, 直接使用双引号括起要显示的字符串即可,根据 C 语言的语法,这些字符串会被转化成常 量数组,数组内存储对应字符的 ASCII 码,然后存储到 STM32 的 FLASH 空间,函数调用 时通过指针来找到对应的 ASCII 码,液晶显示函数使用前面分析过的流程,转换成液晶显 示输出。 在很多场合下,我们可能需要使用液晶屏显示代码中变量的内容,这时很多用户就不 知道该如何解决了,上面的 LCD_Test 函数结尾处演示了如何处理。它主要是使用一个 C 语言标准库里的函数 sprintf,把变量转化成 ASCII 码字符串,转化后的字符串存储到一个 数组中,然后我们再利用液晶显示字符串的函数显示该数组的内容即可。spritnf 函数的用 法与 printf 函数类似,使用它时需要包含头文件 string.h。 28.3.3 显示 GB2312 编码的字符 显示 ASCII 编码比较简单,由于字库文件小,甚至都不需要使用外部的存储器,而显 示汉字时,由于我们的字库是存储到外部存储器上的,这涉及到额外的获取字模数据的操 作,且由于字库制作方式与前面 ASCII 码字库不一样,显示的函数也要作相应的更改。 我们分别制作了两个工程来演示如何显示汉字,以下部分的内容请打开“LTDC—液 晶显示汉字(字库在外部 FLASH)”和“LTDC—LCD 显示汉字(字库在 SD 卡)”工程 阅读理解。这两个工程使用的字库文件内容相同,只是字库存储的位置不一样,工程中我 们把获取字库数据相关的函数代码写在“fonts.c”及“fonts.h” 文件中,字符显示的函数 仍存储在 LCD 驱动文件“bsp_lcd.c”及“bsp_lcd.h”中。 第 389 页 共 928 零死角玩转 STM32—F429 1. 编程要点 (1) 获取字模数据; (2) 根据字模格式,编写液晶显示函数; (3) 编写测试程序,控制液晶汉字。 2. 代码分析 显示汉字字符 由于我们的 GB2312 字库文件与 ASCII 字库文件不是使用同一种方式生成的,所以为 了显示汉字,需要另外编写一个字符显示函数,它利用前文生成的《GB2312_H2424.FON》 字库显示 GB2312 编码里的字符,见代码清单 27-10。 代码清单 28-11 显示 GB2312 编码字符的函数(bsp_ldc.c 文件) 1 2 /***********中文********** 在显示屏上显示的字符大小 ******************/ 3 #define macWIDTH_CH_CHAR 24 //中文字符宽度 4 #define macHEIGHT_CH_CHAR 24 //中文字符高 度 5 6 /** 7 * @brief 在显示器上显示一个中文字符 8 * @param usX :在特定扫描方向下字符的起始 X 坐标 9 * @param usY :在特定扫描方向下字符的起始 Y 坐标 10 * @param usChar :要显示的中文字符(国标码) 11 * @retval 无 12 */ 13 void LCD_DispChar_CH ( uint16_t usX, uint16_t usY, uint16_t usChar) 14 { 15 uint8_t ucPage, ucColumn; 16 uint8_t ucBuffer [ 24*24/8 ]; 17 18 uint32_t usTemp; 19 uint32_t xpos =0; 20 uint32_t Xaddress = 0; 21 22 /*xpos 表示当前行的显存偏移位置*/ 23 xpos = usX*LCD_PIXEL_WIDTH*3; 24 25 /*Xaddress 表示像素点*/ 26 Xaddress += usY; 27 28 macGetGBKCode ( ucBuffer, usChar ); //取字模数据 29 30 /*ucPage 表示当前行数*/ 31 for ( ucPage = 0; ucPage < macHEIGHT_CH_CHAR; ucPage ++ ) 32 { 33 /* 取出 3 个字节的数据,在 lcd 上即是一个汉字的一行 */ 34 usTemp = ucBuffer [ ucPage * 3 ]; 35 usTemp = ( usTemp << 8 ); 36 usTemp |= ucBuffer [ ucPage * 3 + 1 ]; 37 usTemp = ( usTemp << 8 ); 38 usTemp |= ucBuffer [ ucPage * 3 + 2]; 39 40 for ( ucColumn = 0; ucColumn < macWIDTH_CH_CHAR; ucColumn ++ ) 41 { 42 if ( usTemp & ( 0x01 << 23 ) ) //高位在前 第 390 页 共 928 零死角玩转 STM32—F429 43 44 45 xpos) = 46 //GB 47 xpos+2) = 48 //R 49 50 51 52 53 54 xpos) = 55 //GB 56 xpos+2) = 57 //R 58 59 60 61 62 63 64 65 66 67 } 68 } { //字体色 *(__IO uint16_t*)(CurrentFrameBuffer + (3*Xaddress) + (0x00FFFF & CurrentTextColor); *(__IO uint8_t*)(CurrentFrameBuffer + (3*Xaddress) + (0xFF0000 & CurrentTextColor) >> 16; } else { //背景色 *(__IO uint16_t*)(CurrentFrameBuffer + (3*Xaddress) + (0x00FFFF & CurrentBackColor); *(__IO uint8_t*)(CurrentFrameBuffer + (3*Xaddress) + (0xFF0000 & CurrentBackColor) >> 16; } /*指向当前行的下一个点*/ Xaddress++; usTemp <<= 1; } /*显示完一行*/ /*指向字符显示矩阵下一行的第一个像素点*/ Xaddress += (LCD_PIXEL_WIDTH - macWIDTH_CH_CHAR); 这个 GB2312 码的显示函数与 ASCII 码的显示函数是很类似的,它的输入参数有 usX, usY 及 usChar。其中 usX 和 usY 用于设定字符的显示位置,usChar 是字符的编码,这是一 个 16 位的变量,因为 GB2312 编码中每个字符是 2 个字节的。函数的执行流程介绍如下: (6) 使用 xpos 和 Xaddress 来计算字符显示矩阵的偏移。 (7) 使用量 macGetGBKCode 函数获取字模数据,向该函数输入 usChar 参数(字符的编码), 它会从外部 SPI-FLASH 芯片或 SD 卡中读取该字符的字模数据,读取得的数据被存储 到数组 ucBuffer 中。关于 macGetGBKCode 函数我们在后面详细讲解。 (8) 遍历像素点。这个代码在遍历时还使用了 usTemp 变量用来缓存一行的字模数据(本字 模一行有 3 个字节),然后一位一位地判断这些数据,数据位为 1 的时候,像素点就显 示字体颜色,否则显示背景颜色。原理是跟 ASCII 字符显示一样的。 显示中英文字符串 类似地,我们希望希望汉字也能直接以字符串的形式来调用函数显示,而且最好是中 英文字符可以混在一个字符串里。为此,我们编写了 LCD_DisplayStringLine_EN_CH 函数, 代码清单 27-11。 代码清单 28-12 显示中英文的字符串 1 /** 2 * @brief 3 4 * @param 显示一行字符,若超出液晶宽度,不自动换行。 中英混显时,请把英文字体设置为 Font16x24 格式 Line: 要显示的行编号 LINE(0) - LINE(N) 第 391 页 共 928 零死角玩转 STM32—F429 5 * @param *ptr: 要显示的字符串指针 6 * @retval None 7 */ 8 void LCD_DisplayStringLine_EN_CH(uint16_t Line, uint8_t *ptr) 9{ 10 uint16_t refcolumn = 0; 11 /* 判断显示位置不能超出液晶的边界 */ 12 while ((refcolumn < LCD_PIXEL_WIDTH) && 13 ((*ptr != 0) & (((refcolumn + LCD_Currentfonts->Width) & 0xFFFF) >= 14 LCD_Currentfonts->Width))) 15 { 16 /* 使用 LCD 显示一个字符 */ 17 if ( * ptr <= 126 ) //英文字符 18 { 19 20 LCD_DisplayChar(Line, refcolumn, *ptr); 21 /* 根据字体偏移显示的位置 */ 22 refcolumn += LCD_Currentfonts->Width; 23 /* 指向字符串中的下一个字符 */ 24 ptr++; 25 } 26 else //汉字字符 27 { 28 uint16_t usCh; 29 30 /*一个汉字两字节*/ 31 usCh = * ( uint16_t * ) ptr; 32 /*交换字节顺序,stm32 默认小端格式,而国标编码默认大端格式*/ 33 usCh = ( usCh << 8 ) + ( usCh >> 8 ); 34 35 /*显示汉字*/ 36 LCD_DispChar_CH ( Line, refcolumn, usCh ); 37 /*显示位置偏移*/ 38 refcolumn += macWIDTH_CH_CHAR; 39 /* 指向字符串中的下一个字符 */ 40 ptr += 2; 41 } 42 } 43 } 这个函数根据字符串中的编码值,判断它是 ASCII 码还是国标码中的字符,然后作不 同处理。英文部分与前方中的英文字符串显示函数是一样的,中文部分也很类似,需要注 意的是中文字符每个占 2 个字节,而且由于 STM32 芯片的数据是小端格式存储的,国标码 是大端格式存储的,所以函数中对输入参数 ptr 指针获取的编码 usCh 交换了字节顺序,再 输入到单个字符的显示函数 LCD_DispChar_CH 中。 获取 SPI-FLASH 中的字模数据 前面提到的 macGetGBKCode 函数用于获取汉字字模数据,它根据字库文件的存储位 置,有 SPI-FLASH 和 SD 卡两个版本,我们先来分析比较简单的 SPI-FLASH 版本,代码清 单 28-13。该函数定义在“LTDC—液晶显示汉字(字库在外部 FLASH)”工程的“fonts.c” 和“fonts.h”文件中。 代码清单 28-13 从 SPI-FLASH 获取字模数据(“LTDC—液晶显示汉字(字库在外部 FLASH) “工程) 1 2 /*************fonts.h 文件中的定义 **********************************/ 3 第 392 页 共 928 零死角玩转 STM32—F429 4 /*使用 FLASH 字模*/ 5 /*中文字库存储在 FLASH 的起始地址*/ 6 /*FLASH*/ 7 #define GBKCODE_START_ADDRESS 1360*4096 8 9 /*获取字库的函数*/ 10 //定义获取中文字符字模数组的函数名, 11 //ucBuffer 为存放字模数组名, 12 //usChar 为中文字符(国标码) 13 #define macGetGBKCode( ucBuffer, usChar ) \ 14 GetGBKCode_from_EXFlash( ucBuffer, usChar ) 15 int GetGBKCode_from_EXFlash( uint8_t * pBuffer, uint16_t c); 16 /*********************************************************************/ 17 18 /************fonts.c 文件中的字义**************************************/ 19 /*使用 FLASH 字模*/ 20 21 //中文字库存储在 FLASH 的起始地址 : 22 /** 23 * @brief 获取 FLASH 中文显示字库数据 24 * @param pBuffer:存储字库矩阵的缓冲区 25 * @param c : 要获取的文字 26 * @retval None. 27 */ 28 int GetGBKCode_from_EXFlash( uint8_t * pBuffer, uint16_t c) 29 { 30 unsigned char High8bit,Low8bit; 31 unsigned int pos; 32 33 static uint8_t everRead=0; 34 35 /*第一次使用,初始化 FLASH*/ 36 if (everRead == 0) 37 { 38 SPI_FLASH_Init(); 39 everRead=1; 40 } 41 42 High8bit= c >> 8; /* 取高 8 位编码 */ 43 Low8bit= c & 0x00FF; /* 取低 8 位编码*/ 44 45 /*GB2312 公式*/ 46 pos = ((High8bit-0xa1)*94+Low8bit-0xa1)*24*24/8; 47 //读取字模数据 48 SPI_FLASH_BufferRead(pBuffer,GBKCODE_START_ADDRESS+pos,24*24/8); 49 50 return 0; 51 } 这个 macGetGBKCode 实质上是一个宏,当使用 SPI-FLASH 作为字库数据源时, 它等效于 GetGBKCode_from_EXFlash 函数,它的执行过程如下: (1) 初始化 SPI 外设,以使用 SPI 读取 FLASH 的内容,并且初始化后做一个标记,以后再 读取字模数据的时候就不需要再次初始化 SPI 了; (2) 取出要显示字符的 GB2312 编码的高位字节和低位字节,以便后面用于计算字符的字 模地址偏移; (3) 根据字符的编码及字模的大小导出的寻址公式,计算当前要显示字模数据在字库中的 地址偏移; (4) 利用 SPI_FLASH_BufferRead 函数,从 SPI-FLASH 中读取该字模的数据,输入参数中 的 GBKCODE_START_ADDRESS 是在代码头部定义的一个宏,它是字库文件存储在 第 393 页 共 928 零死角玩转 STM32—F429 SPI-FLASH 芯片的基地址,该基地址加上字模在字库中的地址偏移,即可求出字模在 SPI-FLASH 中存储的实际位置。这个基地址具体数值是在我们烧录 FLASH 字库时决 定的,程序中定义的是实验板出厂时默认烧录的位置。 (5) 获取到的字模数据存储到 pBuffer 指针指向的存储空间,显示汉字的函数直接利用它 来显示字符。 获取 SD 卡中的字模数据 类似地,从 SD 卡中获取字模数据时,使用 GetGBKCode_from_sd 函数,见代码清单 28-14。该函数定义在“LTDC—液晶显示汉字(字库在 SD 卡)”工程的“fonts.c”和 “fonts.h”文件中。 代码清单 28-14 从 SD 卡中获取字模数据(“LTDC—液晶显示汉字(字库在 SD 卡)”工程) 1 2 /*使用 SD 字模*/ 3 4 /*SD 卡字模路径*/ 5 #define GBKCODE_FILE_NAME "0:/Font/GB2312_H2424.FON" 6 7 /*获取字库的函数*/ 8 //定义获取中文字符字模数组的函数名, 9 //ucBuffer 为存放字模数组名 10 //usChar 为中文字符(国标码) 11 #define macGetGBKCode( ucBuffer, usChar ) \ 12 GetGBKCode_from_sd( ucBuffer, usChar ) 13 int GetGBKCode_from_sd ( uint8_t * pBuffer, uint16_t c); 14 /*********************************************************************/ 15 16 /************fonts.c 文件中的字义**************************************/ 17 /*使用 SD 字模*/ 18 19 static FIL fnew; /* file objects */ 20 static FATFS fs; /* Work area (file system object) for logical drives */ 21 static FRESULT res_sd; 22 static UINT br; /* File R/W count */ 23 24 /** 25 * @brief 获取 SD 卡中文显示字库数据 26 * @param pBuffer:存储字库矩阵的缓冲区 27 * @param c : 要获取的文字 28 * @retval None. 29 */ 30 int GetGBKCode_from_sd ( uint8_t * pBuffer, uint16_t c) 31 { 32 unsigned char High8bit,Low8bit; 33 unsigned int pos; 34 35 static uint8_t everRead = 0; 36 37 High8bit= c >> 8; /* 取高 8 位数据 */ 38 Low8bit= c & 0x00FF; /* 取低 8 位数据 */ 39 40 pos = ((High8bit-0xa1)*94+Low8bit-0xa1)*24*24/8; 41 42 /*第一次使用,挂载文件系统,初始化 sd*/ 43 if (everRead == 0) 44 { 45 res_sd = f_mount(&fs,"0:",1); 46 everRead = 1; 第 394 页 共 928 零死角玩转 STM32—F429 47 } 48 //GBKCODE_FILE_NAME 是字库文件的路径 49 res_sd = f_open(&fnew , GBKCODE_FILE_NAME, FA_OPEN_EXISTING | FA_READ); 50 51 if ( res_sd == FR_OK ) 52 { 53 f_lseek (&fnew, pos); //指针偏移 54 //24*24 大小的汉字 其字模 占用 24*24/8 个字节 55 res_sd = f_read( &fnew, pBuffer, 24*24/8, &br ); 56 f_close(&fnew); 57 58 return 0; 59 } 60 else 61 return -1; 62 } 63 当字库的数据源在 SD 卡时,macGetGBKCode 宏指向的是这个 GetGBKCode_from_sd 函数。由于字库是使用 SD 卡的文件系统存储的,从 SD 卡中获取字 模数据实质上是直接读取字库文件,利用 f_lseek 函数偏移文件的读取指针,使它能够读取 特定字符的字模数据。 由于使用文件系统的方式读取数据比较慢,而 SD 卡大多数都会使用文件系统,所以 我们一般使用 SPI-FLASH 直接存储字库(不带文件系统地使用),市场上有一些厂商直接生 产专用的字库芯片,可以直接使用,省去自己制作字库的麻烦。 显示 GB2312 字符示例 下面我们再来看 main 文件是如何利用这些函数显示 GB2312 的字符,由于我们用 macGetGBKCode 宏屏蔽了差异,所以在上层使用字符串函数时,不需要针对不同的字库来 源写不同的代码,见代码清单 24-14。 代码清单 28-15 main 函数 1 /** 2 * @brief 主函数 3 * @param 无 4 * @retval 无 5 */ 6 int main(void) 7{ 8 /* LED 端口初始化 */ 9 LED_GPIO_Config(); 10 /*串口初始化*/ 11 Debug_USART_Config(); 12 13 /*使用串口演示如何使用字模,可在上位机查看*/ 14 Printf_Charater(); 15 16 /*初始化液晶屏*/ 17 LCD_Init(); 18 LCD_LayerInit(); 19 LTDC_Cmd(ENABLE); 20 21 /*把背景层刷黑色*/ 22 LCD_SetLayer(LCD_BACKGROUND_LAYER); 23 LCD_Clear(LCD_COLOR_BLACK); 24 25 /*初始化后默认使用前景层*/ 第 395 页 共 928 零死角玩转 STM32—F429 26 27 */ 28 29 30 31 32 33 34 35 36 37 38 } 39 LCD_SetLayer(LCD_FOREGROUND_LAYER); /*默认设置不透明 ,该函数参数为不透明度,范围 0-0xff ,0 为全透明,0xff 为不透明 LCD_SetTransparency(0xFF); LCD_Clear(LCD_COLOR_BLACK); /*经过 LCD_SetLayer(LCD_FOREGROUND_LAYER)函数后, 以下液晶操作都在前景层刷新,除非重新调用过 LCD_SetLayer 函数设置背景层*/ LED_BLUE; Delay(0xfff); while (1) { LCD_Test(); } main 文件中的初始化流程与普通的液晶初始化没有区别,这里也不需要初始化 SPI 或 SDIO,因为我们在获取字库的函数中包含了相应的初始化流程。在 while 循环里调用的 LCD_Test 包含了显示 GB2312 字符串的示例,见代码清单 28-16。 代码清单 28-16 显示 GB2312 字符示例 1 2 /*用于测试各种液晶的函数*/ 3 void LCD_Test(void) 4{ 5 static uint8_t testCNT=0; 6 char dispBuff[100]; 7 8 testCNT++; 9 10 /*使用不透明前景层*/ 11 LCD_SetLayer(LCD_FOREGROUND_LAYER); 12 LCD_SetTransparency(0xff); 13 14 LCD_Clear(LCD_COLOR_BLACK); /* 清屏,显示全黑 */ 15 16 /*设置字体颜色及字体的背景颜色(此处的背景不是指 LCD 的背景层!注意区分)*/ 17 LCD_SetColors(LCD_COLOR_WHITE,LCD_COLOR_BLACK); 18 19 /*选择字体,使用中英文显示时,尽量把英文选择成 16*24 的字体, 20 *中文字体大小是 24*24 的,需要其它字体请自行制作字模*/ 21 /*这个函数只对英文字体起作用*/ 22 LCD_SetFont(&Font16x24); 23 24 LCD_DisplayStringLine_EN_CH(LINE(1),(uint8_t* )"秉火 5.0 英寸液晶屏参数, "); 25 LCD_DisplayStringLine_EN_CH(LINE(2),(uint8_t* )"分辨率:800x480 像素"); 26 LCD_DisplayStringLine_EN_CH(LINE(3),(uint8_t* )"触摸屏:5 点电容触摸屏"); 27 LCD_DisplayStringLine_EN_CH(LINE(4),(uint8_t* )"使用 STM32-LTDC 直接驱动, 28 无需外部 液晶驱动器"); 29 LCD_DisplayStringLine_EN_CH(LINE(5),(uint8_t* )"支持 RGB888/565,24 位数 据 30 总线"); 31 LCD_DisplayStringLine_EN_CH(LINE(6),(uint8_t* )"触摸屏使用 IIC 总线驱动"); 32 33 /*使用 c 标准库把变量转化成字符串*/ 34 sprintf(dispBuff,"显示变量例子: testCount = %d ",testCNT); 35 LCD_ClearLine(LINE(7)); 36 37 /*然后显示该字符串即可,其它变量也是这样处理*/ 第 396 页 共 928 零死角玩转 STM32—F429 38 39 40 } LCD_DisplayStringLine_EN_CH(LINE(7),(uint8_t* )dispBuff); /*以下省略*/ 在调用字符串显示函数的时候,我们也是直接使用双引号括起要显示的中文字符即 可,为什么这样就能正常显示呢?我们的字符串显示函数需要的输入参数是字符的 GB2312 编码,编译器会自动转化这些中文字符成相应的 GB2312 编码吗?为什么编译器不 把它转化成 UTF-8 编码呢?这跟我们的开发环境配置有关,在 MDK 软件中,可在“Edit- >Configuration->Editor->Encoding”选项设定编码,见图 28-6。 图 28-6 MDK 中的字符编码选项 编译环境会把文件中的字符串转换成这里配置的编码,然后存储到 STM32 的程序空间 中,所以这里的设定要与您的字库编码格式一样。如果您的实验板显示的时候出现乱码, 请确保以下所有环节都正常:  SPI-FLASH 或 SD 卡中是否有字库文件?  文件存储的位置或路径是否与程序的配置一致?  开发环境中的字符编码选项是否与字库的编码一致? 28.3.4 显示任意大小的字符 前文中无论是 ASCII 字符还是 GB2312 的字符,都只能显示字库中设定的字体大小, 例如,我们想显示一些像素大小为 48x48 的字符,那我们又得制作相应的字库,非常麻烦。 为此我们编写了一些函数,简便地实现显示任意大小字符的目的。本小节的内容请打开 “LTDC—液晶显示汉字(显示任意大小)”工程来配合阅读。 1. 编程要点 (1) 编写缩放字模数据的函数; (2) 编写利用缩放字模的结果进行字符显示的函数; 第 397 页 共 928 零死角玩转 STM32—F429 (3) 编写测试程序,控制显示不同大小的字符。 2. 代码分析 缩放字模数据 显示任意大小字符的功能,其核心是缩放字模,通过 LCD_zoomChar 函数对原始字模 数据进行缩放,见代码清单 28-1。 代码清单 28-17 缩放字模数据 1 2 #define ZOOMMAXBUFF 16384 3 uint8_t zoomBuff[ZOOMMAXBUFF] = {0}; //用于缩放的缓存,最大支持到 128*128 4 /** 5 * @brief 缩放字模,缩放后的字模由 1 个像素点由 8 个数据位来表示 6 0x01 表示笔迹,0x00 表示空白区 7 * @param in_width :原始字符宽度 8 * @param in_heig :原始字符高度 9 * @param out_width :缩放后的字符宽度 10 * @param out_heig:缩放后的字符高度 11 * @param in_ptr :字库输入指针 注意:1pixel 1bit 12 * @param out_ptr :缩放后的字符输出指针 注意: 1pixel 8bit 13 * out_ptr 实际上没有正常输出,改成了直接输出到全局指针 zoomBuff 中 14 * @param en_cn :0 为英文,1 为中文 15 * @retval 无 16 */ 17 void LCD_zoomChar(uint16_t in_width, //原始字符宽度 18 uint16_t in_heig, //原始字符高度 19 uint16_t out_width, //缩放后的字符宽度 20 uint16_t out_heig, //缩放后的字符高度 21 uint8_t *in_ptr, //字库输入指针 注意:1pixel 1bit 22 uint8_t *out_ptr, //缩放后的字符输出指针 注意: 1pixel 8bit 23 uint8_t en_cn) //0 为英文,1 为中文 24 { 25 uint8_t *pts,*ots; 26 //根据源字模及目标字模大小,设定运算比例因子, 27 //左移 16 是为了把浮点运算转成定点运算 28 unsigned int xrIntFloat_16=(in_width<<16)/out_width+1; 29 unsigned int yrIntFloat_16=(in_heig<<16)/out_heig+1; 30 31 unsigned int srcy_16=0; 32 unsigned int y,x; 33 uint8_t *pSrcLine; 34 uint8_t tempBuff[1024] = {0}; 35 u32 uChar; 36 u16 charBit = in_width / 8; 37 u16 Bitdiff = 32 - in_width; 38 39 //检查参数是否合法 40 if (in_width >= 32) return; //字库不允许超过 32 像素 41 if (in_width * in_heig == 0) return; 42 if (in_width * in_heig >= 1024 ) return; //限制输入最大 32*32 43 44 if (out_width * out_heig == 0) return; 45 if (out_width * out_heig >= ZOOMMAXBUFF ) return; //限制最大缩放 128*128 46 pts = (uint8_t*)&tempBuff; 47 48 //为方便运算,字库的数据由 1 pixel 1bit 映射到 1pixel 8bit 49 //0x01 表示笔迹,0x00 表示空白区 第 398 页 共 928 零死角玩转 STM32—F429 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 } if (en_cn == 0x00) //英文 { //这里以 16 * 24 字库作为测试,其他大小的字库自行根据下列代码做下映射就可以 //英文和中文字库上下边界不对,可在此次调整。需要注意 tempBuff 防止溢出 pts+=in_width*4; for (y=0; y> Bitdiff; for (x=0; x> x) & 0x01; } } } else //中文 { for (y=0; y>16); for (x=0; x>16]; //把源字模数据复制到目标指针中 srcx_16+=xrIntFloat_16; //按比例偏移源像素点 } srcy_16+=yrIntFloat_16; //按比例偏移源像素点 ots+=out_width; } //out_ptr 没有正确传出,后面调用直接改成了全局变量指针 zoomBuff! /*!!!缩放后的字模数据直接存储到全局指针 zoomBuff 里了*/ out_ptr = (uint8_t*)&zoomBuff; 缩放字模的本质是按照缩放比例,减少或增加矩阵中的像素点,见图 28-7,只要把左 侧的矩阵隔一行、隔一列地取出像素点,即可得到右侧按比例缩小了的矩阵,而右侧的小 矩阵按比例填复制像素点即可得到左侧放大的矩阵,上述函数就是完成了这样的工作。 第 399 页 共 928 零死角玩转 STM32—F429 图 28-7 缩放矩阵 该函数的说明如下: (1) 输入参数 函数包含输入参数源字模、缩放后字模的宽度及高度:in_width、inheig、out_width、 out_heig。源字模数据指针 in_ptr,缩放后的字符指针 out_ptr 以及用于指示字模是英文 还是中文的标志 en_cn。其中 out_ptr 指针实质上没有用到,这个函数缩放后的数据最 后直接存储在全局变量 zoomBuff 中了。 第 400 页 共 928 零死角玩转 STM32—F429 (2) 计算缩放比例 根据输入字模与要求的输出字模大小,计算出缩放比例到 xrIntFloat_16 及 yrIntFloat_16 变量中,运算式中的左移 16 位是典型的把浮点型运算转换成定点运算的 处理方式。理解的时候可把左移 16 位的运算去掉,把它当成一个自然的数学小数运算 即可。 (3) 检查输入参数 由于运算变量及数组的一些限制,函数中要检查输入参数的范围,本函数限制最大输 出字模的大小为 128*128 像素,输入字模限制不可以超过 24*24 像素。 (4) 映射字模 输入源的字模都是 1 个数据位表示 1 个像素点的,为方便后面的运算,函数把输入字 模转化成 1 个字节(8 个数据位)表示 1 个像素点,该字节的值为 0x01 表示笔迹像素, 0x00 表示空白像素。把字模数据的 1 个数据位映射为 1 个字节,可以方便后面直接使 用指针和数组索引运算。 (5) 缩放字符 缩放字符这部分代码比较难理解,但总的来说它就是利用前面计算得的比例因子,以 它为步长复制源字模的数据到目标字模的缓冲区中,具体的抽象运算只能意会了。其 中的右移 16 位是把比例因子由定点数转换回原始的数值。如果还是觉得难以理解,可 以把函数的宽度及高度输入参数 in_width、inheig、out_width 及 out_heig 都设置成 24, 然后代入运算来阅读这段代码。 (6) 缩放结果 经过运算,缩放的结果存储在 zoomBuff 中,它只是存储了一个字模的缩放结果,所以 每显示一个字模都需要先调用这个函数更新 zoomBuff 中的字模数据,而且它也是用 1 个字节表示 1 个像素位的。 利用缩放的字模数据显示字符 由于缩放后的字模数据格式与我们原来用的字模数据格式不一样,所以我们也要重新 编写字符显示函数,见代码清单 28-18。 代码清单 28-18 利用缩放的字模显示字符 1 2 /** 3 * @brief 利用缩放后的字模显示字符 4 * @param Xpos :字符显示位置 x 5 * @param Ypos :字符显示位置 y 6 * @param Font_width :字符宽度 7 * @param Font_Heig:字符高度 8 * @param c :要显示的字模数据 9 * @param DrawModel :是否反色显示 10 * @retval 无 11 */ 12 void LCD_DrawChar_Ex(uint16_t Xpos, //字符显示位置 x 13 uint16_t Ypos, //字符显示位置 y 14 uint16_t Font_width, //字符宽度 15 uint16_t Font_Heig, //字符高度 16 uint8_t *c, //字模数据 17 uint16_t DrawModel) //是否反色显示 第 401 页 共 928 零死角玩转 STM32—F429 18 { 19 uint32_t index = 0, counter = 0, xpos =0; 20 uint32_t Xaddress = 0; 21 22 xpos = Xpos*LCD_PIXEL_WIDTH*3; 23 Xaddress += Ypos; 24 25 for (index = 0; index < Font_Heig; index++) 26 { 27 28 for (counter = 0; counter < Font_width; counter++) 29 { 30 if (*c++ == DrawModel) //根据字模及反色设置决定显示哪种颜色 31 { 32 *(__IO uint16_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos) = 33 (0x00FFFF & CurrentBackColor); //GB 34 *(__IO uint8_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos+2) = 35 (0xFF0000 & CurrentBackColor) >> 16; //R 36 } 37 else 38 { 39 *(__IO uint16_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos) = 40 (0x00FFFF & CurrentTextColor); //GB 41 *(__IO uint8_t*)(CurrentFrameBuffer + (3*Xaddress) + xpos+2) = 42 (0xFF0000 & CurrentTextColor) >> 16; //R 43 } 44 Xaddress++; 45 } 46 Xaddress += (LCD_PIXEL_WIDTH - Font_width); 47 } 48 } 49 这个函数主体与前面介绍的字符显示函数都很类似,只是它在判断字模数据位的时候, 直接用一整个字节来判断,区分显示分支,而且还支持了反色显示模式。 利用缩放的字模显示字符串 单个字符显示的函数并不包含字模的获取过程,为便于使用,我们把它直接封装成字 符串显示函数,见代码清单 28-19。 代码清单 28-19 利用缩放的字模显示字符串 1 2 /** 3 * @brief 利用缩放后的字模显示字符串 4 * @param Xpos :字符显示位置 x 5 * @param Ypos :字符显示位置 y 6 * @param Font_width :字符宽度,英文字符在此基础上/2。注意为偶数 7 * @param Font_Heig:字符高度,注意为偶数 8 * @param c :要显示的字符串 9 * @param DrawModel :是否反色显示 10 * @retval 无 11 */ 12 void LCD_DisplayStringLineEx(uint16_t x, //字符显示位置 x 13 uint16_t y, //字符显示位置 y 第 402 页 共 928 零死角玩转 STM32—F429 14 uint16_t Font_width, //要显示的字体宽度,英文字符在此基础上/2。注意为偶 数 15 16 为偶数 uint16_t Font_Heig, //要显示的字体高度,注意 17 uint8_t *ptr, //显示的字符内容 18 uint16_t DrawModel) //是否反色显示 19 { 20 uint16_t refcolumn = x; //x 坐标 21 uint16_t Charwidth; 22 uint8_t *psr; 23 uint8_t Ascii; //英文 24 uint16_t usCh; //中文 25 uint8_t ucBuffer [ 24*24/8 ]; 26 27 while ((refcolumn < LCD_PIXEL_WIDTH) && 28 ((*ptr != 0) & (((refcolumn + LCD_Currentfonts->Width) & 0xFFFF) >= 29 LCD_Currentfonts->Width))) 30 { 31 if (*ptr > 0x80) //如果是中文 32 { 33 Charwidth = Font_width; 34 usCh = * ( uint16_t * ) ptr; 35 usCh = ( usCh << 8 ) + ( usCh >> 8 ); 36 macGetGBKCode ( ucBuffer, usCh ); //取字模数据 37 //缩放字模数据 38 LCD_zoomChar(24,24,Charwidth,Font_Heig,(uint8_t *)&ucBuffer,psr,1); 39 //显示单个字符 40 LCD_DrawChar_Ex(y,refcolumn,Charwidth, 41 Font_Heig, 42 (uint8_t*)&zoomBuff, 43 DrawModel); 44 refcolumn+=Charwidth; 45 ptr+=2; 46 } 47 else 48 { 49 Charwidth = Font_width / 2; 50 Ascii = *ptr - 32; 51 //缩放字模数据 52 LCD_zoomChar(16,24, 53 Charwidth,Font_Heig, 54 (uint8_t *)&LCD_Currentfonts->table[Ascii * LCD_Currentfonts->Height], 55 psr,0); 56 //显示单个字符 57 LCD_DrawChar_Ex(y,refcolumn,Charwidth,Font_Heig, 58 (uint8_t*)&zoomBuff,DrawModel); 59 refcolumn+=Charwidth; 60 ptr++; 61 } 62 } 63 } 这个函数包含了从字符编码到源字模获取、字模缩放及单个字符显示的过程,多个这 样的过程组合起来,就实现了简单易用的字符串显示函数。 利用缩放的字模显示示例 利用缩放的字模显示时,液晶的初始化过程与前面的工程无异,以下我们给出 LCD_Test 函数中调用字符串函数显示不同字符时的示例,见代码清单 28-20。 代码清单 28-20 利用缩放的字模显示示例 第 403 页 共 928 零死角玩转 STM32—F429 1 /*用于测试各种液晶的函数*/ 2 void LCD_Test(void) 3{ 4 static uint8_t testCNT=0; 5 char dispBuff[100]; 6 7 testCNT++; 8 9 /*使用不透明前景层*/ 10 LCD_SetLayer(LCD_FOREGROUND_LAYER); 11 LCD_SetTransparency(0xff); 12 13 LCD_Clear(LCD_COLOR_BLACK); /* 清屏,显示全黑 */ 14 15 /*设置字体颜色及字体的背景颜色(此处的背景不是指 LCD 的背景层!注意区分)*/ 16 LCD_SetColors(LCD_COLOR_WHITE,LCD_COLOR_BLACK); 17 18 LCD_DisplayStringLineEx(0,5,16,16,(uint8_t* )"秉火 F429 16*16 ",0); 19 LCD_DisplayStringLine_EN_CH(LINE(1),(uint8_t* )"秉火 F429 24*24 "); 20 LCD_DisplayStringLineEx(0,50,32,32,(uint8_t* )"秉火 F429 32*32 ",0); 21 LCD_DisplayStringLineEx(0,82,48,48,(uint8_t* )"秉火 F429 48*48 ",0); 22 /*...以下部分省略*/ 23 } 下载验证 用 USB 线连接开发板,编译程序下载到实验板,并上电复位,各个不同的工程会有不 同的的液晶屏显示字符示例。 28.4 每课一问 8. 如果汉字使用字母、笔画和部首应如何编码? (答案:拼音、五笔输入法就是这样编码的) 9. 查阅资料,了解 UTF-16 的四字节编码方式。 (答案:UTF-16 对 Unicode 字符集里的字符用双字节或者四字节进行表示。字符编号 在 0 到 65535 的统一用 2 个字节来表示,将每个字符的编号转换为 2 字节的二进制数, 从 0x0000 到 0xFFFF。 对于编号大于 0xFFFF 的,也就是编号从 0x010000 到 0x10FFFF 的字符,以如下方式进行编码:将该字符的编号减去 0x010000,减去之后 范围从 0x00000 到 0xFFFFF,刚好用 20 个 bit 位 可以表示,将前 10 个 bit 位作为高位 与 0xD800 相加,后 10 个 bit 位作为低位和 0xDC00 相加,因为前 10 个 bit 位在前面补 6 个 0,凑成双字节后,范围是从 0x0000 到 0x03FF,因此加上 0xD800 后 范围从 0xD800 到 0xDBFF,同样的后 10 个 bit 位加上 0xDC00 后范围从 0xDC00 到 0xDFFF, 很巧妙的是,在 Unicode 字符集里,刚好编号 0xD800 到 0xDFFF 没有表示任何字符, 因此在解码的 时候如果发现高位的两个字节是在 0xD800 到 0xDBFF 就知道,这两个 字节应该和低位的 2 个字节作为一个整体,即以四字节为单位进行解析。UCS-2 字符 集是 Unicode 中编号从 0 到 65535 的字符, 编码方式与 UTF-16 的双字节字符编码方 式完全相同,就是对编号从 0 到 65535 的字符,按双字节进行编码。) 10. 使用字模软件自己生成字模并验证,尝试修改字模选项的配置。(可使用串口打印字模 的方式快速验证,把生成的字模替换成原“当”字的字模,并修改打印函数) 第 404 页 共 928 零死角玩转 STM32—F429 11. 找一些网页,查看它的编码方式,除了历史遗留问题,解释为什么目前有的网页仍不 采用 UTF-8 编码。(使用浏览器查看网页源文件,在前几行 HTML 代码中一般会看到 “”这样的代码, 其中 charset 等号后的即为该网页的编码方式) 12. 尝试把 MDK 中的编码格式改成 UTF-8,重新编译下载程序,看看显示中文字符的现 象,为什么数字和英文能正常显示? 第 405 页 共 928 零死角玩转 STM32—F429 第29章 电容触摸屏—触摸画板 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 关于开发板配套的触摸屏参数可查阅《5.0 寸触摸屏面板说明》,触摸面板配套的触摸 控制芯片可查阅《电容触控芯片 GT9157 Datasheet》及《gt91x 编程指南》配套资料获知。 在前面我们学习了如何使用 LTDC 外设控制液晶屏并用它显示各种图形及文字,利用 液晶屏,STM32 的系统具有了高级信息输出功能,然而我们还希望有用户友好的输入设备, 触摸屏是不二之选,目前大部分电子设备都使用触摸屏配合液晶显示器组成人机交互系统。 29.1 触摸屏简介 触摸屏又称触控面板,它是一种把触摸位置转化成坐标数据的输入设备,根据触摸屏 的检测原理,主要分为电阻式触摸屏和电容式触摸屏。相对来说,电阻屏造价便宜,能适 应较恶劣的环境,但它只支持单点触控(一次只能检测面板上的一个触摸位置),触摸时需 要一定的压力,使用久了容易造成表面磨损,影响寿命;而电容屏具有支持多点触控、检 测精度高的特点,电容屏通过与导电物体产生的电容效应来检测触摸动作,只能感应导电 物体的触摸,湿度较大或屏幕表面有水珠时会影响电容屏的检测效果。 第 406 页 共 928 零死角玩转 STM32—F429 图 29-1 单电阻屏、电阻液晶屏(带触摸控制芯片) 图 29-2 单电容屏、电容液晶屏(带触摸控制芯片) 第 407 页 共 928 零死角玩转 STM32—F429 图 29-1 和图 29-2 分别是带电阻触摸屏及电容触摸屏的两种屏幕,从外观上并没有明 显的区别,区分电阻屏与电容屏最直接的方法就是使用绝缘物体点击屏幕,因为电阻屏通 过压力能正常检测触摸动作,而该绝缘物体无法影响电容屏所检测的信号,因而无法检测 到触摸动作。目前电容式触摸屏被大部分应用在智能手机、平板电脑等电子设备中,而在 汽车导航、工控机等设备中电阻式触摸屏仍占主流。 29.1.1 电阻式触摸屏检测原理 电阻式的触摸屏结构见图 29-3。它主要由表面硬涂层、两个 ITO 层、间隔点以及玻璃 底层构成,这些结构层都是透明的,整个触摸屏覆盖在液晶面板上,透过触摸屏可看到液 晶面板。表面涂层起到保护作用,玻璃底层起承载的作用,而两个 ITO 层是触摸屏的关键 结构,它们是涂有铟锡金属氧化物的导电层。两个 ITO 层之间使用间隔点使两层分开,当 触摸屏表面受到压力时,表面弯曲使得上层 ITO 与下层 ITO 接触,在触点处连通电路。 图 29-3 电阻式触摸屏结构 两个 ITO 涂层的两端分别引出 X-、X+、Y-、Y+四个电极,见图 29-4,这是电阻屏最 常见的四线结构,通过这些电极,外部电路向这两个涂层可以施加匀强电场或检测电压。 第 408 页 共 928 零死角玩转 STM32—F429 图 29-4 XY 的 ITO 层结构 当触摸屏被按下时,两个 ITO 层相互接触,从触点处把 ITO 层分为两个电阻,且由于 ITO 层均匀导电,两个电阻的大小与触点离两电极的距离成比例关系,利用这个特性,可 通过以下过程来检测坐标,这也正是电阻触摸屏名称的由来,见图 29-5。  计算 X 坐标时,在 X+电极施加驱动电压 Vref,X-极接地,所以 X+与 X-处形成了匀强 电场,而触点处的电压通过 Y+电极采集得到,由于 ITO 层均匀导电,触点电压与 Vref 之比等于触点 X 坐标与屏宽度之比,从而:  计算 Y 坐标时,在 Y+电极施加驱动电压 Vref,Y-极接地,所以 Y+与 Y-处形成了匀强 电场,而触点处的电压通过 X+电极采集得到,由于 ITO 层均匀导电,触点电压与 Vref 之比等于触点 Y 坐标与屏高度之比,从而: 图 29-5 触摸检测等效电路 为了方便检测触摸的坐标,一些芯片厂商制作了电阻屏专用的控制芯片,控制上述采 集过程、采集电压,外部微控制器直接与触摸控制芯片通讯直接获得触点的电压或坐标。 如图 29-1 中我们生产的这款 3.2 寸电阻触摸屏就是采用 XPT2046 芯片作为触摸控制芯片, 第 409 页 共 928 零死角玩转 STM32—F429 XPT2046 芯片控制 4 线电阻触摸屏,STM32 与 XPT2046 采用 SPI 通讯获取采集得的电压, 然后转换成坐标。 29.1.2 电容式触摸屏检测原理 与电阻式触摸屏不同,电容式触摸屏不需要通过压力使触点变形,再通过触点处电压 值来检测坐标,它的基本原理和前面定时器章节中介绍的电容按键类似,都是利用充电时 间检测电容大小,从而通过检测出电容值的变化来获知触摸信号。见图 29-6,电容屏的最 上层是玻璃(不会像电阻屏那样形变),核心层部分也是由 ITO 材料构成的,这些导电材料 在屏幕里构成了人眼看不见的静电网,静电网由多行 X 轴电极和多列 Y 轴电极构成,两个 电极之间会形成电容。触摸屏工作时,X 轴电极发出 AC 交流信号,而交流信号能穿过电 容,即通过 Y 轴能感应出该信号,当交流电穿越时电容会有充放电过程,检测该充电时间 可获知电容量。若手指触摸屏幕,会影响触摸点附近两个电极之间的耦合,从而改变两个 电极之间的电容量,若检测到某电容的电容量发生了改变,即可获知该电容处有触摸动作 (这就是为什么它被称为电容式触摸屏以及绝缘体触摸没有反应的原因)。 图 29-6 电容触摸屏基本原理 电容屏 ITO 层的结构见图 29-7,这是比较常见的形式,电极由多个菱形导体组成,生 产时使用蚀刻工艺在 ITO 层生成这样的结构。 第 410 页 共 928 零死角玩转 STM32—F429 图 29-7 电容触摸屏的 ITO 层结构 X 轴电极与 Y 轴电极在交叉处形成电容,即这两组电极构成了电容的两极,这样的结 构覆盖了整个电容屏,每个电容单元在触摸屏中都有其特定的物理位置,即电容的位置就 是它在触摸屏的 XY 坐标。检测触摸的坐标时,第 1 条 X 轴的电极发出激励信号,而所有 Y 轴的电极同时接收信号,通过检测充电时间可检测出各个 Y 轴与第 1 条 X 轴相交的各个 互电容的大小,各个 X 轴依次发出激励信号,重复上述步骤,即可得到整个触摸屏二维平 面的所有电容大小。当手指接近时,会导致局部电容改变,根据得到的触摸屏电容量变化 的二维数据表,可以得知每个触摸点的坐标,因此电容触摸屏支持多点触控。 其实电容触摸屏可看作是多个电容按键组合而成,就像机械按键中独立按键和矩阵按 键的关系一样,甚至电容触摸屏的坐标扫描方式与矩阵按键都是很相似的。 29.2 电容触摸屏控制芯片 相对来说,电容屏的坐标检测比电阻屏的要复杂,因而它也有专用芯片用于检测过程, 下面我们以本章重点讲述的电容屏使用的触控芯片 GT9157 为例进行讲解,关于它的详细 说明可从《gt91x 编程指南》和《电容触控芯片 GT9157》文档了解。 29.2.1 GT9157 芯片的引脚 GT9157 芯片的外观可以图 29-2 中找到,其内部结构框图见图 29-8。 第 411 页 共 928 零死角玩转 STM32—F429 图 29-8 GT9157 结构框图 该芯片对外引出的信号线介绍如下: 表 29-1 GT9157 信号线说明 信号线 AVDD 、 AVDD18 、 DVDD12 、 VDDDIO 、 GND Driving channels Sensing channels I2C INT /RSTB 说明 电源和地 激 励信 号输 出的引 脚,一 共有 0-25 个引 脚,它连接到电容屏 ITO 层引出的各个激励 信号轴 信号检测引脚,一共有 0-13 个引脚,它连 接到电容屏 ITO 层引出的各个电容量检测信 号轴 I2C 通信信号线,包含 SCL 与 SDA,外部控 制 器 通 过 它 与 GT9157 芯 片 通 讯 , 配 置 GT9157 的工作方式或获取坐标信号 中断信号,GB9157 芯片通过它告诉外部控 制器有新的触摸事件 复位引脚,用于复位 GT9157 芯片;在上电 时还与 INT 引脚配合设置 IIC 通讯的设备地 址 若您把电容触摸屏与液晶面板分离开来,在触摸面板的背面,可看到它的边框有一些 电路走线,它们就是触摸屏 ITO 层引出的 XY 轴信号线,这些信号线分别引出到 GT9157 芯片的 Driving channels 及 Sensing channels 引脚中。也正是因为触摸屏有这些信号线的存 在,所以手机厂商追求的屏幕无边框是比较难做到的。 29.2.2 上电时序与 I2C 设备地址 GT9157 触控芯片有两个备选的 I2C 通讯地址,这是由芯片的上电时序设定的,见图 29-9。上电时序有 Reset 引脚和 INT 引脚生成,若 Reset 引脚从低电电平转变到高电平期间, INT 引脚为高电平的时候,触控芯片使用的 I2C 设备地址为 0x28/0x29(8 位写、读地址),7 第 412 页 共 928 零死角玩转 STM32—F429 位地址为 0x14;若 Reset 引脚从低电电平转变到高电平期间,INT 引脚一直为低电平,则 触控芯片使用的 I2C 设备地址为 0xBA/0xBB(8 位写、读地址),7 位地址为 0x5D。 图 29-9 GT9157 的上电时序及 I2C 设备地址 29.2.3 寄存器配置 上电复位后,GT9157 芯片需要通过外部主控芯片加载寄存器配置,设定它的工作模式, 这些配置通过 I2C 信号线传输到 GT9157,它的配置寄存器地址都由两个字节来表示,这些 寄存器的地址从 0x8047-0x8100,一般来说,我们实际配置的时候会按照 GT9157 生产厂商 给的默认配置来控制芯片,仅修改部分关键寄存器,见部分寄存器说明见图 29-10。 第 413 页 共 928 零死角玩转 STM32—F429 图 29-10 部分寄存器配置说明 这些寄存器介绍如下: (1) 配置版本寄存器 0x8047 配置版本寄存器,它包含有配置文件的版本号,若新写入的版本号比原版本大, 或者版本号相等,但配置不一样时,才会更新配置文件到寄存器中。其中配置文件是 指记录了寄存器 0x8048-0x80FE 控制参数的一系列数据。 为了保证每次都更新配置,我们一般把配置版本寄存器设置为“0x00”,这样版本号 会默认初始化为‘A’,这样每次我们修改其它寄存器配置的时候,都会写入到 GT9157 中。 (2) X、Y 分辨率 0x8048-0x804B 寄存器用于配置触控芯片输出的 XY 坐标的最大值,为了方便使用, 我们把它配置得跟液晶面板的分辨率一致,这样就能使触控芯片输出的坐标一一对应 到液晶面板的每一个像素点了。 (3) 触点个数 0x804C 触点个数寄存器用于配置它最多可输出多少个同时按下的触点坐标,这个极限 值跟触摸屏面板有关,如我们本章实验使用的触摸面板最多支持 5 点触控。 (4) 模式切换 0x804D 模式切换寄存器中的 X2Y 位可以用于交换 XY 坐标轴;而 INT 触发方式位可 以配置不同的触发方式,当有触摸信号时,INT 引脚会根据这里的配置给出触发信号。 (5) 配置校验 第 414 页 共 928 零死角玩转 STM32—F429 0x80FF 配置校验寄存器用于写入前面 0x8047-0x80FE 寄存器控制参数字节之和的补码, GT9157 收到前面的寄存器配置时,会利用这个数据进行校验,若不匹配,就不会更 新寄存器配置。 (6) 配置更新 0x8100 配置更新寄存器用于控制 GT9157 进行更新,传输了前面的寄存器配置并校验 通过后,对这个寄存器写 1,GT9157 会更新配置。 29.2.4 读取坐标信息 坐标寄存器 上述寄存器主要是由外部主控芯片给 GT9157 写入配置的,而它则使用图 29-11 中的 寄存器向主控器反馈信息。 第 415 页 共 928 零死角玩转 STM32—F429 图 29-11 坐标信息寄存器 (1) 产品 ID 及版本 0x8140-0x8143 寄存器存储的是产品 ID,上电后我们可以利用 I2C 读取这些寄存器的 值来判断 I2C 是否正常通讯,这些寄存器中包含有“9157”字样; 而 0x8144-0x8145 则 保存有固件版本号,不同版本可能不同。 (2) X/Y 分辨率 0x8146-0x8149 寄存器存储了控制触摸屏的分辨率,它们的值与我们前面在配置寄存 器写入的 XY 控制参数一致。所以我们可以通过读取这两个寄存器的值来确认配置 参数是否正确写入。 第 416 页 共 928 零死角玩转 STM32—F429 (3) 状态寄存器 0x814E 地址的是状态寄存器,它的 Buffer status 位存储了坐标状态,当它为 1 时,表 示新的坐标数据已准备好,可以读取,0 表示未就绪,数据无效,外部控制器读取完 坐标后,须对这个寄存器位写 0 。number of touch points 位表示当前有多少个触点。 其余数据位我们不关心。 (4) 坐标数据 从地址 0x814F-0x8156 的是触摸点 1 的坐标数据,从 0x8157-0x815E 的是触摸点 2 的 坐标数据,依次还有存储 3-10 触摸点坐标数据的寄存器。读取这些坐标信息时,我 们通过它们的 track id 来区分笔迹,多次读取坐标数据时,同一个 track id 号里的数 据属于同一个连续的笔划轨迹。 读坐标流程 上电、配置完寄存器后,GT9157 就会开监测触摸屏,若我们前面的配置使 INT 采用 中断上升沿报告触摸信号的方式,整个读取坐标信息的过程如下: (1) 待机时 INT 引脚输出低电平; (2) 有坐标更新时,INT 引脚输出上升沿; (3) INT 输出上升沿后,INT 脚会保持高直到下一个周期(该周期可由配置 Refresh_Rate 决定)。外部主控器在检测到 INT 的信号后,先读取状态寄存器(0x814E)中的 number of touch points 位获当前有多少个触摸点,然后读取各个点的坐标数据,读取 完后将 buffer status 位写为 0。外部主控器的这些读取过程要在一周期内完成,该周 期由 0x8056 地址的 Refresh_Rate 寄存器配置; (4) 上一步骤中 INT 输出上升沿后,若主控未在一个周期内读走坐标,下次 GT9157 即 使检测到坐标更新会再输出一个 INT 脉冲但不更新坐标; (5) 若外部主控一直未读走坐标,则 GT9 会一直输出 INT 脉冲。 29.3 电容触摸屏—触摸画板实验 本小节讲解如何驱动电容触摸屏,并利用触摸屏制作一个简易的触摸画板应用。 学习本小节内容时,请打开配套的“电容触摸屏—触摸画板”工程配合阅读。 29.3.1 硬件设计 图 29-12 液晶屏实物图 第 417 页 共 928 零死角玩转 STM32—F429 本实验使用的液晶电容屏实物见图 27-19,屏幕背面的 PCB 电路对应图 27-21、图 27-25 中的原理图,分别是触摸屏接口及排针接口。 我们这个触摸屏出厂时就与 GT9157 芯片通过柔性电路板连接在一起了,柔性电路板 从 GT9157 芯片引出 VCC、GND、SCL、SDA、RSTN 及 INT 引脚,再通过 FPC 座子引出 到屏幕的 PCB 电路板中,PCB 电路板加了部分电路,如 I2C 的上拉电阻,然后把这些引脚 引出到屏幕右侧的排针处,方便整个屏幕与外部器件相连。 图 29-13 电容屏接口 以上是我们 STM32F429 实验板使用的 5 寸屏原理图,它通过屏幕上的排针接入到实验 板的液晶排母接口,与 STM32 芯片的引脚相连,连接见图 27-25。 图 29-14 屏幕与实验板的引脚连接 图 27-25 中 35-38 号引脚即电容触摸屏相关的控制引脚。 以上原理图可查阅《LCD5.0-黑白原理图》及《秉火 F429 开发板黑白原理图》文档获 知,若您使用的液晶屏或实验板不一样,请根据实际连接的引脚修改程序。 第 418 页 共 928 零死角玩转 STM32—F429 29.3.2 软件设计 本工程中的 GT9157 芯片驱动主要是从官方提供的 Linux 驱动修改过来的,我们把这 部分文件存储到“gt9xx.c”及“gt9xx.h”文件中,而这些驱动的底层 I2C 通讯接口我们存 储到了“bsp_i2c_touch.c”及“bsp_i2c_touch.h”文件中,这些文件也可根据您的喜好命名, 它们不属于 STM32 标准库的内容,是由我们自己根据应用需要编写的。在我们提供的资料 《gt9xx_1.8_drivers.zip》压缩包里有官方的原 Linux 驱动,感兴趣的读者可以对比这些文 件,了解如何移植驱动。 1. 编程要点 (28) 分析官方的 gt9xx 驱动,了解需要提供哪些底层接口; (29) 编写底层驱动接口; (30) 利用 gt9xx 驱动,获取触摸坐标; (31) 编写测试程序检验驱动。 2. 代码分析 触摸屏硬件相关宏定义 根据触摸屏与 STM32 芯片的硬件连接,我们把触摸屏硬件相关的配置都以宏的形式定 义到 “bsp_i2c_touch.h”文件中,见代码清单 24-2。 代码清单 29-1 触摸屏硬件配置相关的宏(bsp_i2c_touch.h 文件) 1 /*设定使用的电容屏 IIC 设备地址*/ 2 #define GTP_ADDRESS 3 4 #define I2CT_FLAG_TIMEOUT 5 #define I2CT_LONG_TIMEOUT 6 7 /*I2C 引脚*/ 8 #define GTP_I2C 9 #define GTP_I2C_CLK 10 #define GTP_I2C_CLK_INIT 11 12 #define GTP_I2C_SCL_PIN 13 #define GTP_I2C_SCL_GPIO_PORT 14 #define GTP_I2C_SCL_GPIO_CLK 15 #define GTP_I2C_SCL_SOURCE 16 #define GTP_I2C_SCL_AF 17 18 #define GTP_I2C_SDA_PIN 19 #define GTP_I2C_SDA_GPIO_PORT 20 #define GTP_I2C_SDA_GPIO_CLK 21 #define GTP_I2C_SDA_SOURCE 22 #define GTP_I2C_SDA_AF 23 24 /*复位引脚*/ 25 #define GTP_RST_GPIO_PORT 26 #define GTP_RST_GPIO_CLK 27 #define GTP_RST_GPIO_PIN 28 /*中断引脚*/ 29 #define GTP_INT_GPIO_PORT 30 #define GTP_INT_GPIO_CLK 0xBA ((uint32_t)0x1000) ((uint32_t)(10 * I2CT_FLAG_TIMEOUT)) I2C2 RCC_APB1Periph_I2C2 RCC_APB1PeriphClockCmd GPIO_Pin_4 GPIOH RCC_AHB1Periph_GPIOH GPIO_PinSource4 GPIO_AF_I2C2 GPIO_Pin_5 GPIOH RCC_AHB1Periph_GPIOH GPIO_PinSource5 GPIO_AF_I2C2 GPIOD RCC_AHB1Periph_GPIOD GPIO_Pin_13 GPIOD RCC_AHB1Periph_GPIOD 第 419 页 共 928 零死角玩转 STM32—F429 31 #define GTP_INT_GPIO_PIN 32 #define GTP_INT_EXTI_PORTSOURCE 33 #define GTP_INT_EXTI_PINSOURCE 34 #define GTP_INT_EXTI_LINE 35 #define GTP_INT_EXTI_IRQ 36 /*中断服务函数*/ 37 #define GTP_IRQHandler GPIO_Pin_12 EXTI_PortSourceGPIOD EXTI_PinSource12 EXTI_Line12 EXTI15_10_IRQn EXTI15_10_IRQHandler 以上代码根据硬件的连接,把与触摸屏通讯使用的引脚号、引脚源以及复用功能映射 都以宏封装起来。在这里还定义了与 GT9157 芯片通讯的 I2C 设备地址,该地址是一个 8 位的写地址,它是由我们的上电时序决定的。 初始化触摸屏控制引脚 利用上面的宏,编写 LTDC 的触摸屏控制引脚的初始化函数,见代码清单 24-3。 代码清单 29-2 触摸屏控制引脚的 GPIO 初始化函数(bsp_i2c_touch.c 文件) 1 /** 2 * @brief 触摸屏 I/O 配置 3 * @param 无 4 * @retval 无 5 */ 6 static void I2C_GPIO_Config(void) 7{ 8 GPIO_InitTypeDef GPIO_InitStructure; 9 10 /*使能 I2C 时钟 */ 11 GTP_I2C_CLK_INIT(GTP_I2C_CLK, ENABLE); 12 13 /*使能触摸屏使用的引脚的时钟*/ 14 RCC_AHB1PeriphClockCmd(GTP_I2C_SCL_GPIO_CLK | GTP_I2C_SDA_GPIO_CLK| 15 GTP_RST_GPIO_CLK|GTP_INT_GPIO_CLK, ENABLE); 16 17 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); 18 19 /* 配置 I2C_SCL 源*/ 20GPIO_PinAFConfig(GTP_I2C_SCL_GPIO_PORT, GTP_I2C_SCL_SOURCE, GTP_I2C_SCL_AF); 21 /* 配置 I2C_SDA 源*/ 22GPIO_PinAFConfig(GTP_I2C_SDA_GPIO_PORT, GTP_I2C_SDA_SOURCE, GTP_I2C_SDA_AF); 23 24 /*配置 SCL 引脚 */ 25 GPIO_InitStructure.GPIO_Pin = GTP_I2C_SCL_PIN; 26 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 27 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 28 GPIO_InitStructure.GPIO_OType = GPIO_OType_OD; 29 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 30 GPIO_Init(GTP_I2C_SCL_GPIO_PORT, &GPIO_InitStructure); 31 32 /*配置 SDA 引脚 */ 33 GPIO_InitStructure.GPIO_Pin = GTP_I2C_SDA_PIN; 34 GPIO_Init(GTP_I2C_SDA_GPIO_PORT, &GPIO_InitStructure); 35 36 /*配置 RST 引脚,下拉推挽输出 */ 37 GPIO_InitStructure.GPIO_Pin = GTP_RST_GPIO_PIN; 38 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 39 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 40 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 41 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; 42 GPIO_Init(GTP_RST_GPIO_PORT, &GPIO_InitStructure); 43 44 /*配置 INT 引脚,下拉推挽输出,方便初始化 */ 45 GPIO_InitStructure.GPIO_Pin = GTP_INT_GPIO_PIN; 第 420 页 共 928 零死角玩转 STM32—F429 46 47 48 49 50 51 } GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; //设置为下拉,方便初始化 GPIO_Init(GTP_INT_GPIO_PORT, &GPIO_InitStructure); 以上函数初始化了触摸屏用到的 I2C 信号线,并且把 RST 及 INT 引脚也初始化成了下 拉推挽输出模式,以便刚上电的时候输出上电时序,设置触摸屏的 I2C 设备地址。 配置 I2C 的模式 接下来需要配置 I2C 的工作模式,GT9157 芯片使用的是标准 7 位地址模式的 I2C 通讯, 所以 I2C 这部分的配置跟我们在 EEPROM 实验中的是一样的,不了解这部分内容的请阅读 EEPROM 章节,见代码清单 24-4。 代码清单 29-3 配置 I2C 工作模式(bsp_i2c_touch.c 文件) 1 2 /* STM32 I2C 快速模式 */ 3 #define I2C_Speed 400000 4 5 /* 这个地址只要与 STM32 外挂的 I2C 器件地址不一样即可 */ 6 #define I2C_OWN_ADDRESS7 0x0A 7 8 /** 9 * @brief I2C 工作模式配置 10 * @param 无 11 * @retval 无 12 */ 13 static void I2C_Mode_Config(void) 14 { 15 I2C_InitTypeDef I2C_InitStructure; 16 17 /* I2C 模式配置 */ 18 I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; 19 I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; 20 I2C_InitStructure.I2C_OwnAddress1 =I2C_OWN_ADDRESS7; 21 I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ; 22 I2C_InitStructure.I2C_AcknowledgedAddress = 23 I2C_AcknowledgedAddress_7bit; /* I2C 的寻址模式 */ 24 I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; */ 25 I2C_Init(GTP_I2C, &I2C_InitStructure); */ 26 */ 27 28 29 } I2C_Cmd(GTP_I2C, ENABLE); I2C_AcknowledgeConfig(GTP_I2C, ENABLE); 使用上电时序设置触摸屏的 I2C 地址 /* 通信速率 /* I2C1 初始化 /* 使能 I2C1 在上面配置完成 STM32 的引脚后,就可以开始控制这些引脚对触摸屏进行控制了,为 了使用 I2C 通讯,首先要根据 GT9157 芯片的上电时序给它设置 I2C 设备地址,见代码清 单 44-7。 代码清单 29-4 使用上电时序设置触摸屏的 I2C 地址(bsp_i2c_touch.c 文件) 1 2 /** 3 * @brief 4 * @param 对 GT91xx 芯片进行复位 无 第 421 页 共 928 零死角玩转 STM32—F429 5 * @retval 无 6 */ 7 void I2C_ResetChip(void) 8{ 9 10 11 GPIO_InitTypeDef GPIO_InitStructure; /*配置 INT 引脚,下拉推挽输出,方便初始化 */ 12 GPIO_InitStructure.GPIO_Pin = GTP_INT_GPIO_PIN; 13 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 14 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 15 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 16 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; 初始化 //设置为下拉,方便 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 } GPIO_Init(GTP_INT_GPIO_PORT, &GPIO_InitStructure); /*初始化 GT9157,rst 为高电平,int 为低电平,则 gt9157 的设备地址被配置为 0xBA*/ /*复位为低电平,为初始化做准备*/ GPIO_ResetBits (GTP_RST_GPIO_PORT,GTP_RST_GPIO_PIN); Delay(0x0FFFFF); /*拉高一段时间,进行初始化*/ GPIO_SetBits (GTP_RST_GPIO_PORT,GTP_RST_GPIO_PIN); Delay(0x0FFFFF); /*把 INT 引脚设置为浮空输入模式,以便接收触摸中断信号*/ GPIO_InitStructure.GPIO_Pin = GTP_INT_GPIO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(GTP_INT_GPIO_PORT, &GPIO_InitStructure); 这段函数中控制 RST 引脚由低电平改变至高电平,且期间 INT 一直为低 电平,这样的上电时序可以控制触控芯片的 I2C 写地址为 0xBA,读地址为 0xBB, 即(0xBA|0x01)。输出完上电时序后,把 STM32 的 INT 引脚模式改成浮空输入模 式,使它可以接收触控芯片输出的触摸中断信号。接下来我们在 I2C_GTP_IRQEnable 函数中使能 INT 中断,见代码清单 29-5。 代码清单 29-5 使能 INT 中断(bsp_i2c_touch.c 文件) 1 2 /** 3 * @brief 使能触摸屏中断 4 * @param 无 5 * @retval 无 6 */ 7 void I2C_GTP_IRQEnable(void) 8{ 9 EXTI_InitTypeDef EXTI_InitStructure; 10 NVIC_InitTypeDef NVIC_InitStructure; 11 GPIO_InitTypeDef GPIO_InitStructure; 12 /*配置 INT 为浮空输入 */ 13 GPIO_InitStructure.GPIO_Pin = GTP_INT_GPIO_PIN; 14 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; 15 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 16 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 17 GPIO_Init(GTP_INT_GPIO_PORT, &GPIO_InitStructure); 18 19 /* 连接 EXTI 中断源 到 INT 引脚 */ 20 SYSCFG_EXTILineConfig(GTP_INT_EXTI_PORTSOURCE, GTP_INT_EXTI_PINSOURCE); 21 第 422 页 共 928 零死角玩转 STM32—F429 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 } /* 选择 EXTI 中断源 */ EXTI_InitStructure.EXTI_Line = GTP_INT_EXTI_LINE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); /* 配置中断优先级 */ NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); /*使能中断*/ NVIC_InitStructure.NVIC_IRQChannel = GTP_INT_EXTI_IRQ; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); 这个 INT 引脚我们配置为上升沿触发,是跟后面写入到触控芯片的配置参数一致的。 初始化封装 利用以上函数,我们把信号引脚及 I2C 设备地址初始化的过程都封装到函数 I2C_Touch_Init 中,见代码清单 29-6。 代码清单 29-6 封装引脚初始化及上电时序(bsp_i2c_touch.c 文件) 1 2 /** 3 * @brief I2C 外设(GT91xx)初始化 4 * @param 无 5 * @retval 无 6 */ 7 void I2C_Touch_Init(void) 8{ 9 I2C_GPIO_Config(); 10 11 I2C_Mode_Config(); 12 13 I2C_ResetChip(); 14 15 I2C_GTP_IRQEnable(); 16 } I2C 基本读写函数 为了与上层“gt9xx.c”驱动文件中的函数对接,本实验中的 I2C 读写函数与 EEPROM 实验中的有稍微不同,见代码清单 27-8。 代码清单 29-7 I2C 基本读写函数(bsp_i2c_touch.c 文件) 1 2 __IO uint32_t I2CTimeout = I2CT_LONG_TIMEOUT; 3 /** 4 * @brief IIC 等待超时调用本函数输出调试信息 5 * @param None. 6 * @retval 返回 0xff,表示 IIC 读取数据失败 7 */ 8 static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode) 9{ 10 /* Block communication and all processes */ 11 GTP_ERROR("I2C 等待超时!errorCode = %d",errorCode); 12 return 0xFF; 13 } 14 /** 第 423 页 共 928 零死角玩转 STM32—F429 15 * @brief 使用 IIC 读取数据 16 * @param 17 * @arg ClientAddr:从设备地址 18 * @arg pBuffer:存放由从机读取的数据的缓冲区指针 19 * @arg NumByteToRead:读取的数据长度 20 * @retval 无 21 */ 22 uint32_t I2C_ReadBytes(uint8_t ClientAddr,uint8_t* pBuffer, 23 uint16_t NumByteToRead) 24 { 25 I2CTimeout = I2CT_LONG_TIMEOUT; 26 27 while (I2C_GetFlagStatus(GTP_I2C, I2C_FLAG_BUSY)) 28 { 29 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0); 30 } 31 32 /* Send STRAT condition */ 33 I2C_GenerateSTART(GTP_I2C, ENABLE); 34 35 I2CTimeout = I2CT_FLAG_TIMEOUT; 36 37 /* Test on EV5 and clear it */ 38 while (!I2C_CheckEvent(GTP_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 39 { 40 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1); 41 } 42 /* Send GT91xx address for read */ 43 I2C_Send7bitAddress(GTP_I2C, ClientAddr, I2C_Direction_Receiver); 44 45 I2CTimeout = I2CT_FLAG_TIMEOUT; 46 47 /* Test on EV6 and clear it */ 48 while (!I2C_CheckEvent(GTP_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) 49 { 50 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2); 51 } 52 /* While there is data to be read */ 53 while (NumByteToRead) 54 { 55 if (NumByteToRead == 1) 56 { 57 /* Disable Acknowledgement */ 58 I2C_AcknowledgeConfig(GTP_I2C, DISABLE); 59 60 /* Send STOP Condition */ 61 I2C_GenerateSTOP(GTP_I2C, ENABLE); 62 } 63 I2CTimeout = I2CT_LONG_TIMEOUT; 64 while (I2C_CheckEvent(GTP_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED)==0) 65 { 66 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3); 67 } 68 { 69 /* Read a byte from the device */ 70 *pBuffer = I2C_ReceiveData(GTP_I2C); 71 72 /* Point to the next location where the byte read will be saved */ 73 pBuffer++; 74 75 /* Decrement the read bytes counter */ 76 NumByteToRead--; 77 } 78 } 79 /* Enable Acknowledgement to be ready for another reception */ 80 I2C_AcknowledgeConfig(GTP_I2C, ENABLE); 第 424 页 共 928 零死角玩转 STM32—F429 81 return 0; 82 } 83 84 85 86 87 /** 88 * @brief 使用 IIC 写入数据 89 * @param 90 * @arg ClientAddr:从设备地址 91 * @arg pBuffer:缓冲区指针 92 * @arg NumByteToWrite:写的字节数 93 * @retval 无 94 */ 95 uint32_t I2C_WriteBytes(uint8_t ClientAddr,uint8_t* pBuffer, 96 uint8_t NumByteToWrite) 97 { 98 I2CTimeout = I2CT_LONG_TIMEOUT; 99 100 while (I2C_GetFlagStatus(GTP_I2C, I2C_FLAG_BUSY)) 101 { 102 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4); 103 } 104 105 /* Send START condition */ 106 I2C_GenerateSTART(GTP_I2C, ENABLE); 107 108 I2CTimeout = I2CT_FLAG_TIMEOUT; 109 110 /* Test on EV5 and clear it */ 111 while (!I2C_CheckEvent(GTP_I2C, I2C_EVENT_MASTER_MODE_SELECT)) 112 { 113 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5); 114 } 115 116 /* Send GT91xx address for write */ 117 I2C_Send7bitAddress(GTP_I2C, ClientAddr, I2C_Direction_Transmitter); 118 119 I2CTimeout = I2CT_FLAG_TIMEOUT; 120 121 /* Test on EV6 and clear it */ 122 while(!I2C_CheckEvent(GTP_I2C,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) 123 { 124 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6); 125 } 126 /* While there is data to be written */ 127 while (NumByteToWrite--) 128 { 129 /* Send the current byte */ 130 I2C_SendData(GTP_I2C, *pBuffer); 131 132 /* Point to the next byte to be written */ 133 pBuffer++; 134 135 I2CTimeout = I2CT_FLAG_TIMEOUT; 136 137 /* Test on EV8 and clear it */ 138 while (!I2C_CheckEvent(GTP_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED)) 139 { 140 if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7); 141 } 142 } 143 /* Send STOP condition */ 144 I2C_GenerateSTOP(GTP_I2C, ENABLE); 145 return 0; 146 } 第 425 页 共 928 零死角玩转 STM32—F429 这里的读写函数都是很纯粹的 I2C 通讯过程,即读函数只有读过程,不包含发送寄存 器地址的过程,而写函数也是只有写过程,没有包含寄存器的地址,大家可以对比一下它 们与前面 EEPROM 实验中的差别。这两个函数都只包含从 I2C 的设备地址、缓冲区指针以 及数据量。 Linux 的 I2C 驱动接口 使用前面的基本读写函数,主要是为了对接原“gt9xx.c”驱动里使用的 Linux I2C 接 口函数 I2C_Transfer,实现了这个函数后,移植时就可以减少“gt9xx.c”文件的修改量。 I2C_Transfer 函数见代码清单 27-9。 代码清单 29-8 Linux 的 I2C 驱动接口(gt9xx.c 文件) 1 2 /* 表示读数据 */ 3 #define I2C_M_RD 0x0001 4 /* 5 * 存储 I2C 通讯的信息 6 * @addr: 从设备的 I2C 设备地址 7 * @flags: 控制标志 8 * @len: 读写数据的长度 9 * @buf: 存储读写数据的指针 10 **/ 11 struct i2c_msg 12 { 13 uint8_t addr; /*从设备的 I2C 设备地址 */ 14 uint16_t flags; /*控制标志*/ 15 uint16_t len; /*读写数据的长度 */ 16 uint8_t *buf; /*存储读写数据的指针 */ 17 }; 18 19 /** 20 * @brief 使用 IIC 进行数据传输 21 * @param 22 * @arg i2c_msg:数据传输结构体 23 * @arg num:数据传输结构体的个数 24 * @retval 正常完成的传输结构个数,若不正常,返回 0xff 25 */ 26 static int I2C_Transfer( struct i2c_msg *msgs,int num) 27 { 28 int im = 0; 29 int ret = 0; 30 //输出调试信息,可忽略 31 GTP_DEBUG_FUNC(); 32 33 for (im = 0; ret == 0 && im != num; im++) 34 { 35 //根据 flag 判断是读数据还是写数据 36 if ((msgs[im].flags&I2C_M_RD)) 37 { 38 //IIC 读取数据 39 ret = I2C_ReadBytes(msgs[im].addr, msgs[im].buf, msgs[im].len); 40 } 41 else 42 { 43 //IIC 写入数据 44 ret = I2C_WriteBytes(msgs[im].addr, msgs[im].buf, msgs[im].len); 45 } 46 } 第 426 页 共 928 零死角玩转 STM32—F429 47 48 49 50 51 52 } if (ret) return ret; return im; //正常完成的传输结构个数 I2C_Transfer 的主要输入参数是 i2c_msg 结构体的指针以及要传输多少个这样的结构体。 i2c_msg 结构体包含以下几个成员: (1) addr 这是从机的 I2C 设备地址,通讯时无论是读方向还是写方向,给这个成员赋值为 写地址即可(本实验中为 0xBA)。 (2) flags 这个成员存储了控制标志,它用于指示本 i2c_msg 结构体要求以什么方式来传输。 在原 Linux 驱动中有很多种控制方式,在我们这个工程中,只支持读或写控制标 志,flags 被赋值为 I2C_M_RD 宏的时候表示读方向,其余值表示写方向。 (3) len 本成员存储了要读写的数据长度。 (4) buf 本成员存储了指向读写数据缓冲区的指针。 利用这个结构体,我们再来看 I2C_Transfer 函数做了什么工作。 (1) 输入参数中可能包含有多个要传输的 i2c_msg 结构体,利用 for 循环把这些结构体 一个个地传输出去; (2) 传输的时候根据 i2c_msg 结构体中的 flags 标志,确定应该调用 I2C 读函数还是写 函数,这些函数即前面定义的 I2C 基本读写函数。调用这些函数的时候,以 i2c_msg 结构体的成员作为参数。 I2C 复合读写函数 理解了 I2C_Transfer 函数的代码,我们发现它还是什么都没做,只是对 I2C 基本读写 函数封装了比较特别的调用形式而已,而我们知道 GT9157 触控芯片都有很多不同的寄存 器,如果我们仅用上面的函数,如何向特定寄存器写入参数或读取特定寄存器的内容呢? 这就需要再利用 I2C_Transfer 函数编写具有 I2C 通讯复合时序的读写函数了。Linux 驱动进 行这样的封装是为了让它的核心层与具体设备独立开来,对于这个巨型系统,这样写代码 是很有必要的,上述的 I2C_Transfer 函数属于 Linux 内部的驱动层,它对外提供接口,而 像 GT9157、EEPROM 等使用 I2C 的设备,都利用这个接口编写自己具体的驱动文件, GT9157 的这些 I2C 复合读写函数见代码清单 27-10。 代码清单 29-9 I2C 复合读写函数(gt9xx.c 文件) 1 //寄存器地址的长度 2 #define GTP_ADDR_LENGTH 2 3 4 /** 5 * @brief 从 IIC 设备中读取数据 6 * @param 7* @arg client_addr:设备地址 8* @arg buf[0~1]: 读取数据寄存器的起始地址 第 427 页 共 928 零死角玩转 STM32—F429 9* @arg buf[2~len-1]: 存储读出来数据的缓冲 buffer 10 * @arg len: GTP_ADDR_LENGTH + read bytes count( 11 寄存器地址长度+读取的数据字节数) 12 * @retval i2c_msgs 传输结构体的个数,2 为成功,其它为失败 13 */ 14 static int32_t GTP_I2C_Read(uint8_t client_addr, uint8_t *buf, 15 int32_t len) 16 { 17 struct i2c_msg msgs[2]; 18 int32_t ret=-1; 19 int32_t retries = 0; 20 21 //输出调试信息,可忽略 22 GTP_DEBUG_FUNC(); 23 /*一个读数据的过程可以分为两个传输过程: 24 * 1. IIC 写入 要读取的寄存器地址 25 * 2. IIC 读取 数据 26 * */ 27 28 msgs[0].flags = !I2C_M_RD; //写入 29 msgs[0].addr = client_addr; //IIC 设备地址 30 msgs[0].len = GTP_ADDR_LENGTH; //寄存器地址为 2 字节(即写入两字节的数据) 31 msgs[0].buf = &buf[0]; //buf[0~1]存储的是要读取的寄存器地址 32 33 msgs[1].flags = I2C_M_RD; //读取 34 msgs[1].addr = client_addr; //IIC 设备地址 35 msgs[1].len = len - GTP_ADDR_LENGTH; //要读取的数据长度 36 msgs[1].buf = &buf[GTP_ADDR_LENGTH]; //buf[GTP_ADDR_LENGTH]之后的缓 冲区存储读出的数据 37 38 while (retries < 5) // 39 { 40 ret = I2C_Transfer( msgs, 2); //调用 IIC 数据传输过程函数,有 2 个传输过程 41 if (ret == 2)break; 42 retries++; 43 } 44 if ((retries >= 5)) 45 { 46 //发送失败,输出调试信息 47 GTP_ERROR("I2C Read Error"); 48 } 49 return ret; 50 } 51 52 /** 53 * @brief 向 IIC 设备写入数据 54 * @param 55 * @arg client_addr:设备地址 56 * @arg buf[0~1]: 要写入的数据寄存器的起始地址 57 * @arg buf[2~len-1]: 要写入的数据 58 * @arg len: GTP_ADDR_LENGTH + write bytes count( 59 寄存器地址长度+写入的数据字节数) 60 * @retval i2c_msgs 传输结构体的个数,1 为成功,其它为失败 61 */ 62 static int32_t GTP_I2C_Write(uint8_t client_addr,uint8_t *buf, 63 int32_t len) 64 { 65 struct i2c_msg msg; 66 int32_t ret = -1; 67 int32_t retries = 0; 68 69 //输出调试信息,可忽略 70 GTP_DEBUG_FUNC(); 第 428 页 共 928 零死角玩转 STM32—F429 71 72 73 74 75 76 节数) 77 78 79 80 81 程 82 83 84 85 86 87 88 89 90 91 } /*一个写数据的过程只需要一个传输过程: * 1. IIC 连续 写入 数据寄存器地址及数据 * */ msg.flags = !I2C_M_RD; //写入 msg.addr = client_addr; //从设备地址 msg.len = len; //长度直接等于(寄存器地址长度+写入的数据字 msg.buf = buf; //直接连续写入缓冲区中的数据(包括了寄存器地址) while (retries < 5) { ret = I2C_Transfer(&msg, 1); //调用 IIC 数据传输过程函数,1 个传输过 if (ret == 1)break; retries++; } if ((retries >= 5)) { //发送失败,输出调试信息 GTP_ERROR("I2C Write Error"); } return ret; 可以看到,复合读写函数都包含有 client_addr、buf 及 len 输入参数,其中 client_addr 表示 I2C 的设备地址,buf 存储了要读写的寄存器地址及数据,len 表示 buf 的长度。在函 数的内部处理中,复合读写过程被分解成两个基本的读写过程,输入参数被转化存储到 i2c_msg 结构体中,每个基本读写过程使用一个 i2c_msg 结构体来表示,见表 29-2 和表 29-3。 表 29-2 复合读过程的步骤分解 复合读过程的步骤分解 说明 传输寄存器地址 这相当于一个 I2C 的基本写过程,写入一个 2 字节长度的 寄存器地址,buf 指针的前两个字节内容被解释为寄存器地 址。 从寄存器读取内容 这是一个 I2C 的基本读过程,读取到的数据存储到 buf 指 针的第 3 个地址开始的空间中。 表 29-3 复合写过程的步骤分解 复合写过程的步骤分解 说明 传输寄存器地址 这相当于一个 I2C 的基本写过程,写入一个 2 字节长度的 寄存器地址,buf 指针的前两个字节内容被解释为寄存器地 址。 向寄存器写入内容 这也是一个 I2C 的基本写过程,写入的数据为 buf 指针的 第 3 个地址开始的内容。 复合过程的分解主要是针对寄存器地址传输和实际数据传输来划分的,调用这两个复 合读写过程的时候,我们需要注意 buf 的前两个字节为寄存器地址,且 len 的长度为 buf 的 整体长度。 读取触控芯片的产品 ID 及版本号 利用上述复合读写函数,我们就可以使用 I2C 控制触控芯片了,首先是最简单的读取 版本函数,见代码清单 27-11。 代码清单 29-10 读取触控芯片的产品 ID 及版本号(gt9xx.c 文件) 第 429 页 共 928 零死角玩转 STM32—F429 1 2 /*设定使用的电容屏 IIC 设备地址*/ 3 #define GTP_ADDRESS 0xBA 4 //芯片版本号地址 5 #define GTP_REG_VERSION 0x8140 6 7 /******************************************************* 8 Function: 9 Read chip version. 10 Input: 11 client: i2c device 12 version: buffer to keep ic firmware version 13 Output: 14 read operation return. 15 2: succeed, otherwise: failed 16 *******************************************************/ 17 int32_t GTP_Read_Version(void) 18 { 19 int32_t ret = -1; 20 //寄存器地址 21 uint8_t buf[8] = {GTP_REG_VERSION >> 8, GTP_REG_VERSION & 0xff}; 22 //输出调试信息,可忽略 23 GTP_DEBUG_FUNC(); 24 25 ret = GTP_I2C_Read(GTP_ADDRESS, buf, sizeof(buf)); 26 if (ret < 0) 27 { 28 GTP_ERROR("GTP read version failed"); 29 return ret; 30 } 31 32 if (buf[5] == 0x00) 33 { 34 GTP_INFO("IC Version: %c%c%c_%02x%02x", 35 buf[2], buf[3], buf[4], buf[7], buf[6]); 36 } 37 else 38 { 39 GTP_INFO("IC Version: %c%c%c%c_%02x%02x", 40 buf[2], buf[3], buf[4], buf[5], buf[7], buf[6]); 41 } 42 return ret; 43 } 这个函数定义了一个 8 字节的 buf 数组,并且向它的第 0 和第 1 个元素写入产品 ID 寄 存器的地址,然后调用复合读取函数,即可从芯片中读取这些寄存器的信息,结果使用宏 GTP_INFO 输出。 向触控芯片写入配置参数 万事俱备,现在我们可以使用 I2C 向触摸芯片写入寄存器配置了,见代码清单 29-11。 代码清单 29-11 初始化并向触控芯片写入配置参数(gt9xx.c 文件) 1 2 /*寄存器参数表*/ 3 #define CTP_CFG_GROUP1 {\ 4 0x00,0x20,0x03,0xE0,0x01,0x05,0x3C,0x00,0x01,0x08,\ 5 0x28,0x0C,0x50,0x32,0x03,0x05,0x00,0x00,0x00,0x00,\ 6 0x00,0x00,0x00,0x17,0x19,0x1E,0x14,0x8B,0x2B,0x0D,\ 7 0x33,0x35,0x0C,0x08,0x00,0x00,0x00,0x9A,0x03,0x11,\ 8 0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x32,0x00,0x00,\ 9 0x00,0x20,0x58,0x94,0xC5,0x02,0x00,0x00,0x00,0x04,\ 10 0xB0,0x23,0x00,0x93,0x2B,0x00,0x7B,0x35,0x00,0x69,\ 11 0x41,0x00,0x5B,0x4F,0x00,0x5B,0x00,0x00,0x00,0x00,\ 12 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\ 第 430 页 共 928 零死角玩转 STM32—F429 13 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\ 14 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\ 15 0x00,0x00,0x02,0x04,0x06,0x08,0x0A,0x0C,0x0E,0x10,\ 16 0x12,0x14,0x16,0x18,0x1A,0xFF,0x00,0x00,0x00,0x00,\ 17 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,\ 18 0x00,0x00,0x00,0x02,0x04,0x06,0x08,0x0A,0x0C,0x0F,\ 19 0x10,0x12,0x13,0x16,0x18,0x1C,0x1D,0x1E,0x1F,0x20,\ 20 0x21,0x22,0x24,0x26,0xFF,0xFF,0xFF,0xFF,0x00,0x00,\ 21 0x00,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,\ 22 0xFF,0xFF,0xFF,0xFF,0x48,0x01\ 23 } 24 25 /*用于计算参数表中的数据长度*/ 26 #define CFG_GROUP_LEN(p_cfg_grp) (sizeof(p_cfg_grp) / sizeof(p_cfg_grp[0])) 27 28 /******************************************************* 29 Function: 30 Initialize gtp. 31 Input: 32 ts: goodix private data 33 Output: 34 Executive outcomes. 35 0: succeed, otherwise: failed 36 *******************************************************/ 37 int32_t GTP_Init_Panel(void) 38 { 39 int32_t ret = -1; 40 41 int32_t i = 0; 42 uint8_t check_sum = 0; 43 int32_t retry = 0; 44 45 uint8_t cfg_info[] = CTP_CFG_GROUP1; 46 uint8_t cfg_info_len =CFG_GROUP_LEN(cfg_info) ; 47 48 uint8_t cfg_num =0x80FE-0x8047+1 ; //需要配置的寄存器个数 49 50 I2C_Touch_Init(); 51 52 GTP_Read_Version(); 53 54 memset(&config[GTP_ADDR_LENGTH], 0, GTP_CONFIG_MAX_LENGTH); 55 memcpy(&config[GTP_ADDR_LENGTH], cfg_info, cfg_info_len); 56 57 //计算要写入 checksum 寄存器的值 58 check_sum = 0; 59 for (i = GTP_ADDR_LENGTH; i < cfg_num+GTP_ADDR_LENGTH; i++) 60 { 61 check_sum += config[i]; 62 } 63 config[ cfg_num+GTP_ADDR_LENGTH] = (~check_sum) + 1; //checksum 64 config[ cfg_num+GTP_ADDR_LENGTH+1] = 1; //refresh 配置更新标志 65 66 //写入配置信息 67 for (retry = 0; retry < 5; retry++) 68 { 69 ret = GTP_I2C_Write(GTP_ADDRESS, config , cfg_num + GTP_ADDR_LENGTH+2); 70 if (ret > 0) 71 { 72 break; 73 } 74 } 75 Delay(0xfffff); //延迟等待芯片更新 76 77 /*使能中断,这样才能检测触摸数据*/ 第 431 页 共 928 零死角玩转 STM32—F429 78 79 80 } I2C_GTP_IRQEnable(); return 0; 这段代码调用 I2C_Touch_Init 初始化了 STM32 的 I2C 外设,设定触控芯片的 I2C 设备 地址,然后调用了 GTP_Read_Version 尝试获取触控芯片的版本号。接下来是函数的主体, 它使用 GTP_I2C_Write 函数通过 I2C 把配置参数表 CTP_CFG_GROUP1 写入到触控芯片的 的配置寄存器中,注意传输中包含有 checksum 寄存器的值。写入完参数后调用 I2C_GTP_IRQEnable 以使能 INT 引脚检测中断。 INT 中断服务函数 经过上面的函数初始化后,触摸屏就可以开始工作了,当触摸时,INT 引脚会产生触 摸中断,会进入中断服务函数 GTP_IRQHandler,见代码清单 29-12。 代码清单 29-12 触摸屏的中断服务函数(stm32f4xx_it.c 文件) 1 //触摸屏中断服务函数 2 void GTP_IRQHandler(void) 3{ 4 //确保是否产生了 EXTI Line 中断 5 if (EXTI_GetITStatus(GTP_INT_EXTI_LINE) != RESET) 6 { 7 GTP_TouchProcess(); 8 EXTI_ClearITPendingBit(GTP_INT_EXTI_LINE); 9 } 10 } //清除中断标志位 中断服务函数只是简单地调用了 GTP_TouchProcess 函数,它是读取触摸坐标的主体。 读取坐标数据 GTP_TouchProcess 函数的内容见代码清单 29-13。 代码清单 29-13 GTP_TouchProcess 坐标读取函数 1 2 /*状态寄存器地址*/ 3 #define GTP_READ_COOR_ADDR 0x814E 4 5 /** 6 * @brief 触屏处理函数,轮询或者在触摸中断调用 7 * @param 无 8 * @retval 无 9 */ 10 static void Goodix_TS_Work_Func(void) 11 { 12 uint8_t end_cmd[3] = {GTP_READ_COOR_ADDR >> 8, GTP_READ_COOR_ADDR & 0xFF, 0}; 13 uint8_t point_data[2 + 1 + 8 * GTP_MAX_TOUCH + 1]= {GTP_READ_COOR_ADDR >> 8, 14 GTP_READ_COOR_ADDR & 0xFF }; 15 16 uint8_t touch_num = 0; 17 uint8_t finger = 0; 18 static uint16_t pre_touch = 0; 19 static uint8_t pre_id[GTP_MAX_TOUCH] = {0}; 20 21 uint8_t client_addr=GTP_ADDRESS; 22 uint8_t* coor_data = NULL; 23 int32_t input_x = 0; 24 int32_t input_y = 0; 25 int32_t input_w = 0; 26 uint8_t id = 0; 27 第 432 页 共 928 零死角玩转 STM32—F429 28 int32_t i = 0; 29 int32_t ret = -1; 30 31 GTP_DEBUG_FUNC(); 32 33 ret = GTP_I2C_Read(client_addr, point_data, 12);//10 字节寄存器加 2 字节地 址 34 if (ret < 0) 35 { 36 GTP_ERROR("I2C transfer error. errno:%d\n ", ret); 37 return; 38 } 39 40 finger = point_data[GTP_ADDR_LENGTH];//状态寄存器数据 41 42 if (finger == 0x00) //没有数据,退出 43 { 44 return; 45 } 46 47 if ((finger & 0x80) == 0) //判断 buffer status 位 48 { 49 goto exit_work_func;//坐标未就绪,数据无效 50 } 51 52 touch_num = finger & 0x0f;//坐标点数 53 if (touch_num > GTP_MAX_TOUCH) 54 { 55 goto exit_work_func;//大于最大支持点数,错误退出 56 } 57 58 if (touch_num > 1)//不止一个点 59 { 60 uint8_t buf[8 * GTP_MAX_TOUCH] = {(GTP_READ_COOR_ADDR + 10) >> 8, 61 (GTP_READ_COOR_ADDR + 10) & 0xff}; 62 63 64 ret = GTP_I2C_Read(client_addr, buf, 2 + 8 * (touch_num - 1)); 65 //复制其余点数的数据到 point_data memcpy(&point_data[12], &buf[2], 8 * (touch_num - 1)); 66 } 67 68 if (pre_touch>touch_num) 点释放了 //pre_touch>touch_num,表示有的 69 { 70 for (i = 0; i < pre_touch; i++) //一个点一个点处理 71 { 72 uint8_t j; 73 for (j=0; j= touch_num-1)//遍历当前所有 id 都找不到 pre_id[i],表示已释放 81 { 82 GTP_Touch_Up( pre_id[i]); 83 } 84 } 85 } 86 } 87 88 if (touch_num) 89 { 第 433 页 共 928 零死角玩转 STM32—F429 90 for (i = 0; i < touch_num; i++) 理 //一个点一个点处 91 { 92 coor_data = &point_data[i * 8 + 3]; 93 94 id = coor_data[0] & 0x0F; //track id 95 pre_id[i] = id; 96 97 input_x = coor_data[1] | (coor_data[2] << 8); //x 坐标 98 input_y = coor_data[3] | (coor_data[4] << 8); //y 坐标 99 input_w = coor_data[5] | (coor_data[6] << 8); //size 100 101 { 102 GTP_Touch_Down( id, input_x, input_y, input_w);//数据处理 103 } 104 } 105 } 106 else if (pre_touch) //touch_ num=0 且 pre_touch!=0 107 { 108 for (i=0; i Y = (3.3 * X ) / 2^12。 第 444 页 共 928 零死角玩转 STM32—F429 30.3 ADC 初始化结构体详解 标准库函数对每个外设都建立了一个初始化结构体 xxx_InitTypeDef(xxx 为外设名称), 结构体成员用于设置外设工作参数,并由标准库函数 xxx_Init()调用这些设定参数进入设置 外设相应的寄存器,达到配置外设工作环境的目的。 结构体 xxx_InitTypeDef 和库函数 xxx_Init 配合使用是标准库精髓所在,理解了结构体 xxx_InitTypeDef 每个成员意义基本上就可以对该外设运用自如了。结构体 xxx_InitTypeDef 定义在 stm32f4xx_xxx.h 文件中,库函数 xxx_Init 定义在 stm32f4xx_xxx.c 文件中,编程时 我们可以结合这两个文件内注释使用。 ADC_InitTypeDef 结构体 ADC_InitTypeDef 结构体定义在 stm32f4xx_adc.h 文件内,具体定义如下: 1 typedef struct { 2 uint32_t ADC_Resolution; 3 FunctionalState ADC_ScanConvMode; 4 FunctionalState ADC_ContinuousConvMode; 5 uint32_t ADC_ExternalTrigConvEdge; 6 uint32_t ADC_ExternalTrigConv; 7 uint32_t ADC_DataAlign; 8 uint8_t ADC_NbrOfChannel; 9 } ADC_InitTypeDef; //ADC 分辨率选择 //ADC 扫描选择 //ADC 连续转换模式选择 //ADC 外部触发极性 //ADC 外部触发选择 //输出数据对齐方式 //转换通道数目 ADC_Resolution:配置 ADC 的分辨率,可选的分辨率有 12 位、10 位、8 位和 6 位。 分辨率越高,AD 转换数据精度越高,转换时间也越长;分辨率越低,AD 转换数据精度越 低,转换时间也越短。 ScanConvMode:可选参数为 ENABLE 和 DISABLE,配置是否使用扫描。如果是单通 道 AD 转换使用 DISABLE,如果是多通道 AD 转换使用 ENABLE。 ADC_ContinuousConvMode:可选参数为 ENABLE 和 DISABLE,配置是启动自动连 续转换还是单次转换。使用 ENABLE 配置为使能自动连续转换;使用 DISABLE 配置为单 次转换,转换一次后停止需要手动控制才重新启动转换。 ADC_ExternalTrigConvEdge:外部触发极性选择,如果使用外部触发,可以选择触发 的极性,可选有禁止触发检测、上升沿触发检测、下降沿触发检测以及上升沿和下降沿均 可触发检测。 ADC_ExternalTrigConv:外部触发选择,图 30-1 中列举了很多外部触发条件,可根据 项目需求配置触发来源。实际上,我们一般使用软件自动触发。 ADC_DataAlign:转换结果数据对齐模式,可选右对齐 ADC_DataAlign_Right 或者左 对齐 ADC_DataAlign_Left。一般我们选择右对齐模式。 ADC_NbrOfChannel:AD 转换通道数目。 ADC_CommonInitTypeDef 结构体 ADC 除了有 ADC_InitTypeDef 初始化结构体外,还有一个 ADC_CommonInitTypeDef 通用初始化结构体。ADC_CommonInitTypeDef 结构体内容决定三个 ADC 共用的工作环境, 比如模式选择、ADC 时钟等等。 ADC_CommonInitTypeDef 结构体也是定义在 stm32_f4xx.h 文件中,具体定义如下: 第 445 页 共 928 零死角玩转 STM32—F429 1 typedef struct { 2 uint32_t ADC_Mode; 3 uint32_t ADC_Prescaler; 4 uint32_t ADC_DMAAccessMode; 5 uint32_t ADC_TwoSamplingDelay; 6 } ADC_InitTypeDef; //ADC 模式选择 //ADC 分频系数 //DMA 模式配置 //采样延迟 ADC_Mode:ADC 工作模式选择,有独立模式、双重模式以及三重模式。 ADC_Prescaler:ADC 时钟分频系数选择,ADC 时钟是有 PCLK2 分频而来,分频系数 决定 ADC 时钟频率,可选的分频系数为 2、4、6 和 8。ADC 最大时钟配置为 36MHz。 ADC_DMAAccessMode:DMA 模式设置,只有在双重或者三重模式才需要设置,可 以设置三种模式,具体可参考参考手册说明。 ADC_TwoSamplingDelay:2 个采样阶段之前的延迟,仅适用于双重或三重交错模式。 30.4 独立模式单通道采集实验 STM32 的 ADC 功能繁多,我们设计三个实验尽量完整的展示 ADC 的功能。首先是比 较基础实用的单通道采集,实现开发板上电位器的动触点输出引脚电压的采集并通过串口 打印至 PC 端串口调试助手。单通道采集适用 AD 转换完成中断,在中断服务函数中读取 数据,不使用 DMA 传输,在多通道采集时才使用 DMA 传输。 30.4.1 硬件设计 开发板板载一个贴片滑动变阻器,电路设计见图 30-5。 图 30-5 开发板电位器部分原理图 贴片滑动变阻器的动触点通过连接至 STM32 芯片的 ADC 通道引脚。当我们使用旋转 滑动变阻器调节旋钮时,其动触点电压也会随之改变,电压变化范围为 0~3.3V,亦是开发 板默认的 ADC 电压采集范围。 第 446 页 共 928 零死角玩转 STM32—F429 30.4.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。 我们编写两个 ADC 驱动文件,bsp_adc.h 和 bsp_adc.c,用来存放 ADC 所用 IO 引脚的 初始化函数以及 ADC 配置相关函数。 1. 编程要点 1) 初始化配置 ADC 目标引脚为模拟输入模式; 2) 使能 ADC 时钟; 3) 配置通用 ADC 为独立模式,采样 4 分频; 4) 设置目标 ADC 为 12 位分辨率,1 通道的连续转换,不需要外部触发; 5) 设置 ADC 转换通道顺序及采样时间; 6) 配置使能 ADC 转换完成中断,在中断内读取转换完数据; 7) 启动 ADC 转换; 8) 使能软件触发 ADC 转换。 ADC 转换结果数据使用中断方式读取,这里没有使用 DMA 进行数据传输。 2. 代码分析 ADC 宏定义 代码清单 30-1 ADC 宏定义 1 #define Rheostat_ADC_IRQ 2 #define Rheostat_ADC_INT_FUNCTION 3 4 #define RHEOSTAT_ADC_GPIO_PORT 5 #define RHEOSTAT_ADC_GPIO_PIN 6 #define RHEOSTAT_ADC_GPIO_CLK 7 8 #define RHEOSTAT_ADC 9 #define RHEOSTAT_ADC_CLK 10 #define RHEOSTAT_ADC_CHANNEL ADC_IRQn ADC_IRQHandler GPIOC GPIO_Pin_3 RCC_AHB1Periph_GPIOC ADC1 RCC_APB2Periph_ADC1 ADC_Channel_13 使用宏定义引脚信息方便硬件电路改动时程序移植。 ADC GPIO 初始化函数 代码清单 30-2 ADC GPIO 初始化 1 static void Rheostat_ADC_GPIO_Config(void) 第 447 页 共 928 零死角玩转 STM32—F429 2{ 3 4 5 6 7 8 9 10 11 12 13 14 15 } GPIO_InitTypeDef GPIO_InitStructure; // 使能 GPIO 时钟 RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_GPIO_CLK, ENABLE); // 配置 IO GPIO_InitStructure.GPIO_Pin = RHEOSTAT_ADC_GPIO_PIN; // 配置为模拟输入 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 不上拉不下拉 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ; GPIO_Init(RHEOSTAT_ADC_GPIO_PORT, &GPIO_InitStructure); 使用到 GPIO 时候都必须开启对应的 GPIO 时钟,GPIO 用于 AD 转换功能必须配置为 模拟输入模式。 配置 ADC 工作模式 代码清单 30-3 ADC 工作模式配置 1 static void Rheostat_ADC_Mode_Config(void) 2{ 3 ADC_InitTypeDef ADC_InitStructure; 4 ADC_CommonInitTypeDef ADC_CommonInitStructure; 5 6 // 开启 ADC 时钟 7 RCC_APB2PeriphClockCmd(RHEOSTAT_ADC_CLK , ENABLE); 8 9 // -------------------ADC Common 结构体 参数 初始化-------------------- 10 // 独立 ADC 模式 11 ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; 12 // 时钟为 fpclk x 分频 13 ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; 14 // 禁止 DMA 直接访问模式 15 ADC_CommonInitStructure.ADC_DMAAccessMode=ADC_DMAAccessMode_Disabled; 16 // 采样时间间隔 17 ADC_CommonInitStructure.ADC_TwoSamplingDelay= 18 ADC_TwoSamplingDelay_10Cycles; 19 ADC_CommonInit(&ADC_CommonInitStructure); 20 21 // -------------------ADC Init 结构体 参数 初始化--------------------- 22 // ADC 分辨率 23 ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; 24 // 禁止扫描模式,多通道采集才需要 25 ADC_InitStructure.ADC_ScanConvMode = DISABLE; 26 // 连续转换 27 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; 28 //禁止外部边沿触发 29 ADC_InitStructure.ADC_ExternalTrigConvEdge = 30 ADC_ExternalTrigConvEdge_None; 31 //使用软件触发,外部触发不用配置,注释掉即可 32 //ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_T1_CC1; 33 //数据右对齐 34 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; 35 //转换通道 1 个 36 ADC_InitStructure.ADC_NbrOfConversion = 1; 37 ADC_Init(RHEOSTAT_ADC, &ADC_InitStructure); 38 //------------------------------------------------------------------ 39 // 配置 ADC 通道转换顺序为 1,第一个转换,采样时间为 56 个时钟周期 40 ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL, 41 1, ADC_SampleTime_56Cycles); 42 第 448 页 共 928 零死角玩转 STM32—F429 43 44 45 46 47 48 49 } // ADC 转换结束产生中断,在中断服务程序中读取转换值 ADC_ITConfig(RHEOSTAT_ADC, ADC_IT_EOC, ENABLE); // 使能 ADC ADC_Cmd(RHEOSTAT_ADC, ENABLE); //开始 adc 转换,软件触发 ADC_SoftwareStartConv(RHEOSTAT_ADC); 首先,使用 ADC_InitTypeDef 和 ADC_CommonInitTypeDef 结构体分别定义一个 ADC 初始化和 ADC 通用类型变量,这两个结构体我们之前已经有详细讲解。 我们调用 RCC_APB2PeriphClockCmd()开启 ADC 时钟。 接下来我们使用 ADC_CommonInitTypeDef 结构体变量 ADC_CommonInitStructure 来 配置 ADC 为独立模式、分频系数为 4、不需要设置 DMA 模式、20 个周期的采样延迟,并 调用 ADC_CommonInit 函数完成 ADC 通用工作环境配置。 我们使用 ADC_InitTypeDef 结构体变量 ADC_InitStructure 来配置 ADC1 为 12 位分辨 率、单通道采集不需要扫描、启动连续转换、使用内部软件触发无需外部触发事件、使用 右对齐数据格式、转换通道为 1,并调用 ADC_Init 函数完成 ADC1 工作环境配置。 ADC_RegularChannelConfig 函数用来绑定 ADC 通道转换顺序和时间。它接收 4 个形 参,第一个形参选择 ADC 外设,可为 ADC1、ADC2 或 ADC3;第二个形参通道选择,总 共可选 18 个通道;第三个形参为转换顺序,可选为 1 到 16;第四个形参为采样周期选择, 采样周期越短,ADC 转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC 转换数据输出周期就越长同时数据精度越高。PC3 对应 ADC 通道 ADC_Channel_13,这里 我们选择 ADC_SampleTime_56Cycles 即 56 周期的采样时间。 利用 ADC 转换完成中断可以非常方便的保证我们读取到的数据是转换完成后的数据 而不用担心该数据可能是 ADC 正在转换时“不稳定”的数据。我们使用 ADC_ITConfig 函 数使能 ADC 转换完成中断,并在中断服务函数中读取转换结果数据。 ADC_Cmd 函数控制 ADC 转换启动和停止。 最后,如果使用软件触发需要调用 ADC_SoftwareStartConvCmd 函数进行使能配置。 ADC 中断配置 代码清单 30-4 ADC 中断配置 1 static void Rheostat_ADC_NVIC_Config(void) 2{ 3 NVIC_InitTypeDef NVIC_InitStructure; 4 5 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); 6 7 NVIC_InitStructure.NVIC_IRQChannel = Rheostat_ADC_IRQ; 8 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 9 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; 10 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 11 12 NVIC_Init(&NVIC_InitStructure); 13 } 在 Rheostat_ADC_NVIC_Config 函数中我们配置了 ADC 转换完成中断,使用中断同时 需要配置中断源和中断优先级。 第 449 页 共 928 零死角玩转 STM32—F429 ADC 中断服务函数 代码清单 30-5 ADC 中断服务函数 1 void ADC_IRQHandler(void) 2{ 3 if (ADC_GetITStatus(RHEOSTAT_ADC,ADC_IT_EOC)==SET) { 4 // 读取 ADC 的转换值 5 ADC_ConvertedValue = ADC_GetConversionValue(RHEOSTAT_ADC); 6 7 } 8 ADC_ClearITPendingBit(RHEOSTAT_ADC,ADC_IT_EOC); 9 10 } 中断服务函数一般定义在 stm32f4xx_it.c 文件内,我们只使能了 ADC 转换完成中断, 在 ADC 转换完成后就会进入中断服务函数,我们在中断服务函数内直接读取 ADC 转换结 果保存在变量 ADC_ConvertedValue(在 main.c 中定义)中。 ADC_GetConversionValue 函数是获取 ADC 转换结果值的库函数,只有一个形参为 ADC 外设,可选为 ADC1、ADC2 或 ADC3,该函数还返回一个 16 位的 ADC 转换结果值。 主函数 代码清单 30-6 主函数 1 int main(void) 2{ 3 /*初始化 USART1*/ 4 Debug_USART_Config(); 5 6 /* 初始化滑动变阻器用到的 DAC,ADC 采集完成会产生 ADC 中断, 7 在 stm32f4xx_it.c 文件中的中断服务函数更新 ADC_ConvertedValue 的值 */ 8 Rheostat_Init(); 9 10 printf("\r\n ----这是一个 ADC 实验(NO DMA 传输)----\r\n"); 11 12 13 while (1) { 14 Delay(0xffffee); 15 printf("\r\n The current AD value = 0x%04X \r\n", 16 ADC_ConvertedValue); 17 printf("\r\n The current AD value = %f V \r\n",ADC_Vol); 18 19 ADC_Vol =(float)(ADC_ConvertedValue*3.3/4096); // 读取转换的 AD 值 20 21 } 22 } 主函数先调用 USARTx_Config 函数配置调试串口相关参数,函数定义在 bsp_debug_usart.c 文件中。 接下来调用 Rheostat _Init 函数进行 ADC 初始化配置并启动 ADC。Rheostat _Init 函数 是定义在 bsp_adc.c 文件中,它只是简单的分别调用 Rheostat_ADC_GPIO_Config ()、 Rheostat_ADC_Mode_Config ()和 Rheostat_ADC_NVIC_Config()。 Delay 函数只是一个简单的延时函数。 在 ADC 中断服务函数中我们把 AD 转换结果保存在变量 ADC_ConvertedValue 中,根 据我们之前的分析可以非常清楚的计算出对应的电位器动触点的电压值。 第 450 页 共 928 零死角玩转 STM32—F429 最后就是把相关数据打印至串口调试助手。 30.4.3 下载验证 用 USB 线连接开发板“USB TO UART”接口跟电脑,在电脑端打开串口调试助手, 把编译好的程序下载到开发板。在串口调试助手可看到不断有数据从开发板传输过来,此 时我们旋转电位器改变其电阻值,那么对应的数据也会有变化。 30.5 独立模式多通道采集实验 30.5.1 硬件设计 开发板已通过排针接口把部分 ADC 通道引脚引出,我们可以根据需要选择使用。实 际使用时候必须注意保存 ADC 引脚是单独使用的,不可能与其他模块电路共用同一引脚。 30.5.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。 跟单通道例程一样,我们编写两个 ADC 驱动文件,bsp_adc.h 和 bsp_adc.c,用来存放 ADC 所用 IO 引脚的初始化函数以及 ADC 配置相关函数,实际上这两个文件跟单通道实验 的文件是非常相似的。 1. 编程要点 1) 初始化配置 ADC 目标引脚为模拟输入模式; 2) 使能 ADC 时钟和 DMA 时钟; 3) 配置 DMA 从 ADC 规矩数据寄存器传输数据到我们指定的存储区; 4) 配置通用 ADC 为独立模式,采样 4 分频; 5) 设置 ADC 为 12 位分辨率,启动扫描,连续转换,不需要外部触发; 6) 设置 ADC 转换通道顺序及采样时间; 7) 使能 DMA 请求,DMA 在 AD 转换完自动传输数据到指定的存储区; 8) 启动 ADC 转换; 9) 使能软件触发 ADC 转换。 ADC 转换结果数据使用 DMA 方式传输至指定的存储区,这样取代单通道实验使用中 断服务的读取方法。实际上,多通道 ADC 采集一般使用 DMA 数据传输方式更加高效方便。 第 451 页 共 928 零死角玩转 STM32—F429 2. 代码分析 ADC 宏定义 代码清单 30-7 多通道 ADC 相关宏定义 1 //转换的通道个数 2 #define RHEOSTAT_NOFCHANEL 4 3 4 #define RHEOSTAT_ADC_DR_ADDR ((u32)ADC3+0x4c) 5 #define RHEOSTAT_ADC_GPIO_PORT GPIOF 6 #define RHEOSTAT_ADC_GPIO_CLK RCC_AHB1Periph_GPIOF 7 #define RHEOSTAT_ADC ADC3 8 #define RHEOSTAT_ADC_CLK RCC_APB2Periph_ADC3 9 10 #define RHEOSTAT_ADC_GPIO_PIN1 GPIO_Pin_6 11 #define RHEOSTAT_ADC_CHANNEL1 ADC_Channel_4 12 13 #define RHEOSTAT_ADC_GPIO_PIN2 GPIO_Pin_7 14 #define RHEOSTAT_ADC_CHANNEL2 ADC_Channel_5 15 16 #define RHEOSTAT_ADC_GPIO_PIN3 GPIO_Pin_8 17 #define RHEOSTAT_ADC_CHANNEL3 ADC_Channel_6 18 19 #define RHEOSTAT_ADC_GPIO_PIN4 GPIO_Pin_9 20 #define RHEOSTAT_ADC_CHANNEL4 ADC_Channel_7 21 22 // DMA2 数据流 0 通道 2 23 #define RHEOSTAT_ADC_DMA_CLK RCC_AHB1Periph_DMA2 24 #define RHEOSTAT_ADC_DMA_CHANNEL DMA_Channel_2 25 #define RHEOSTAT_ADC_DMA_STREAM DMA2_Stream0 定义 4 个通道进行多通道 ADC 实验,并且定义 DMA 相关配置。 ADC GPIO 初始化函数 代码清单 30-8 ADC GPIO 初始化 1 static void Rheostat_ADC_GPIO_Config(void) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 5 // 使能 GPIO 时钟 6 RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_GPIO_CLK, ENABLE); 7 8 // 配置 IO 9 GPIO_InitStructure.GPIO_Pin = RHEOSTAT_ADC_GPIO_PIN1 | 10 RHEOSTAT_ADC_GPIO_PIN2 | 11 RHEOSTAT_ADC_GPIO_PIN3 | 12 RHEOSTAT_ADC_GPIO_PIN4; 13 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; 14 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ; //不上拉不下拉 15 GPIO_Init(RHEOSTAT_ADC_GPIO_PORT, &GPIO_InitStructure); 16 } 使用到 GPIO 时候都必须开启对应的 GPIO 时钟,GPIO 用于 AD 转换功能必须配置为 模拟输入模式。 配置 ADC 工作模式 代码清单 30-9 ADC 工作模式配置 1 static void Rheostat_ADC_Mode_Config(void) 第 452 页 共 928 零死角玩转 STM32—F429 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 DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; ADC_CommonInitTypeDef ADC_CommonInitStructure; // 开启 ADC 时钟 RCC_APB2PeriphClockCmd(RHEOSTAT_ADC_CLK , ENABLE); // 开启 DMA 时钟 RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_DMA_CLK, ENABLE); // ------------------DMA Init 结构体参数 初始化-----------------------// 选择 DMA 通道,通道存在于数据流中 DMA_InitStructure.DMA_Channel = RHEOSTAT_ADC_DMA_CHANNEL; // 外设基址为:ADC 数据寄存器地址 DMA_InitStructure.DMA_PeripheralBaseAddr = RHEOSTAT_ADC_DR_ADDR; // 存储器地址 DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)ADC_ConvertedValue; // 数据传输方向为外设到存储器 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; // 缓冲区大小,指一次传输的数据项 DMA_InitStructure.DMA_BufferSize = RHEOSTAT_NOFCHANEL; // 外设寄存器只有一个,地址不用递增 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 存储器地址递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; // // 外设数据大小为半字,即两个字节 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 存储器数据大小也为半字,跟外设数据大小相同 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 循环传输模式 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // DMA 传输通道优先级为高,当使用一个 DMA 通道时,优先级设置不影响 DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 禁止 DMA FIFO ,使用直连模式 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; // FIFO 阈值大小,FIFO 模式禁止时,这个不用配置 DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // 存储器突发单次传输 DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; // 外设突发单次传输 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //初始化 DMA 数据流,流相当于一个大的管道,管道里面有很多通道 DMA_Init(RHEOSTAT_ADC_DMA_STREAM, &DMA_InitStructure); // 使能 DMA 数据流 DMA_Cmd(RHEOSTAT_ADC_DMA_STREAM, ENABLE); // -------------------ADC Common 结构体 参数 初始化-------------------// 独立 ADC 模式 ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; // 时钟为 fpclk x 分频 ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; // 禁止 DMA 直接访问模式 ADC_CommonInitStructure.ADC_DMAAccessMode=ADC_DMAAccessMode_Disabled; 第 453 页 共 928 零死角玩转 STM32—F429 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 } // 采样时间间隔 ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_10Cycles; ADC_CommonInit(&ADC_CommonInitStructure); // -------------------ADC Init 结构体 参数 初始化--------------------// ADC 分辨率 ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 开启扫描模式 ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //禁止外部边沿触发 ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; //使用软件触发,外部触发不用配置,注释掉即可 //ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_T1_CC1; //数据右对齐 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //转换通道 x 个 ADC_InitStructure.ADC_NbrOfConversion = RHEOSTAT_NOFCHANEL; ADC_Init(RHEOSTAT_ADC, &ADC_InitStructure); //------------------------------------------------------------------ // 配置 ADC 通道转换顺序和采样时间周期 ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL1, 1, ADC_SampleTime_56Cycles); ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL2, 2, ADC_SampleTime_56Cycles); ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL3, 3, ADC_SampleTime_56Cycles); ADC_RegularChannelConfig(RHEOSTAT_ADC, RHEOSTAT_ADC_CHANNEL4, 4, ADC_SampleTime_56Cycles); // 使能 DMA 请求 after last transfer (Single-ADC mode) ADC_DMARequestAfterLastTransferCmd(RHEOSTAT_ADC, ENABLE); // 使能 ADC DMA ADC_DMACmd(RHEOSTAT_ADC, ENABLE); // 使能 ADC ADC_Cmd(RHEOSTAT_ADC, ENABLE); //开始 adc 转换,软件触发 ADC_SoftwareStartConv(RHEOSTAT_ADC); 首先,我们使用了 DMA_InitTypeDef 定义了一个 DMA 初始化类型变量,该结构体内 容我们在 DMA 篇已经做了非常详细的讲解;另外还使用 ADC_InitTypeDef 和 ADC_CommonInitTypeDef 结构体分别定义一个 ADC 初始化和 ADC 通用类型变量,这两 个结构体我们之前已经有详细讲解。 调用 RCC_APB2PeriphClockCmd()开启 ADC 时钟以及 RCC_AHB1PeriphClockCmd()开 启 DMA 时钟。 我们需要对 DMA 进行必要的配置。首先设置外设基地址就是 ADC 的规则数据寄存器 地址;存储器的地址就是我们指定的数据存储区空间,ADC_ConvertedValue 是我们定义的 一个全局数组名,它是一个无符号 16 位含有 4 个元素的整数数组;ADC 规则转换对应只 有一个数据寄存器所以地址不能递增,而我们定义的存储区是专门用来存放不同通道数据 的,所以需要自动地址递增。ADC 的规则数据寄存器只有低 16 位有效,实际存放的数据 只有 12 位而已,所以设置数据大小为半字大小。ADC 配置为连续转换模式 DMA 也设置为 循环传输模式。设置好 DMA 相关参数后就使能 DMA 的 ADC 通道。 第 454 页 共 928 零死角玩转 STM32—F429 接下来我们使用 ADC_CommonInitTypeDef 结构体变量 ADC_CommonInitStructure 来 配置 ADC 为独立模式、分频系数为 4、不需要设置 DMA 模式、20 个周期的采样延迟,并 调用 ADC_CommonInit 函数完成 ADC 通用工作环境配置。 我们使用 ADC_InitTypeDef 结构体变量 ADC_InitStructure 来配置 ADC1 为 12 位分辨 率、使能扫描模式、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐数 据格式、转换通道为 4,并调用 ADC_Init 函数完成 ADC3 工作环境配置。 ADC_RegularChannelConfig 函数用来绑定 ADC 通道转换顺序和采样时间。分别绑定 四个 ADC 通道引脚并设置相应的转换顺序。 ADC_DMARequestAfterLastTransferCmd 函数控制是否使能 ADC 的 DMA 请求,如果 使能请求,并调用 ADC_DMACmd 函数使能 DMA,则在 ADC 转换完成后就请求 DMA 实 现数据传输。 ADC_Cmd 函数控制 ADC 转换启动和停止。 最后,如果使用软件触发需要调用 ADC_SoftwareStartConvCmd 函数进行使能配置。 主函数 代码清单 30-10 主函数 1 int main(void) 2{ 3 /*初始化 USART1*/ 4 Debug_USART_Config(); 5 6 /* 初始化滑动变阻器用到的 DAC 7 ,ADC 数据采集完成后直接由 DMA 运输数据到 ADC_ConvertedValue 变量 8 DMA 直接改变 ADC_ConvertedValue 的值*/ 9 Rheostat_Init(); 10 11 printf("\r\n ----这是一个 ADC 实验(多通道采集)----\r\n"); 12 13 while (1) { 14 Delay(0xffffff); 15 ADC_ConvertedValueLocal[0]=(float)(ADC_ConvertedValue[0]*3.3/4096); 16 ADC_ConvertedValueLocal[1]=(float)(ADC_ConvertedValue[1]*3.3/4096); 17 ADC_ConvertedValueLocal[2]=(float)(ADC_ConvertedValue[2]*3.3/4096); 18 ADC_ConvertedValueLocal[3]=(float)(ADC_ConvertedValue[3]*3.3/4096); 19 20 printf("\r\n CH1_PF6 value = %fV\r\n",ADC_ConvertedValueLocal[0]); 21 printf("\r\n CH2_PF7 value = %fV\r\n",ADC_ConvertedValueLocal[1]); 22 printf("\r\n CH3_PF8 value = %fV\r\n",ADC_ConvertedValueLocal[2]); 23 printf("\r\n CH4_PF9 value = %fV\r\n",ADC_ConvertedValueLocal[3]); 24 25 printf("\r\n"); 26 } 27 } 主 函 数 先 调 用 USARTx_Config 函 数 配 置 调 试 串 口 相 关 参 数 , 函 数 定 义 在 bsp_debug_usart.c 文件中。 接下来调用 Rheostat_Init 函数进行 ADC 初始化配置并启动 ADC。Rheostat_Init 函数是 定 义 在 bsp_adc.c 文 件 中 , 它 只 是 简 单 的 分 别 调 用 Rheostat_ADC_GPIO_Config() 和 Rheostat_ADC_Mode_Config ()。 Delay 函数只是一个简单的延时函数。 第 455 页 共 928 零死角玩转 STM32—F429 我 们 配 置 了 DMA 数 据 传 输 所 以 它 会 自 动 把 ADC 转 换 完 成 后 数 据 保 存 到 数 组 ADC_ConvertedValue 内,我们只要直接使用数组就可以了。经过简单地计算就可以得到每 个通道对应的实际电压。 最后就是把相关数据打印至串口调试助手。 30.5.3 下载验证 将待测电压通过杜邦线接在对应引脚上,用 USB 线连接开发板“USB TO UART”接 口跟电脑,在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手 可看到不断有数据从开发板传输过来,此时我们改变输入电压值,那么对应的数据也会有 变化。 30.6 三重 ADC 交替模式采集实验 AD 转换包括采样阶段和转换阶段,在采样阶段才对通道数据进行采集;而在转换阶 段只是将采集到的数据进行转换为数字量输出,此刻通道数据变化不会改变转换结果。独 立模式的 ADC 采集需要在一个通道采集并且转换完成后才会进行下一个通道的采集。双 重或者三重 ADC 的机制使用两个或以上 ADC 同时采样两个或以上不同通道的数据或者使 用两个或以上 ADC 交叉采集同一通道的数据。双重或者三重 ADC 模式较独立模式一个最 大的优势就是转换速度快。 我们这里只介绍三重 ADC 交替模式,关于双重或者三重 ADC 的其他模式与之类似, 可以参考三重 ADC 交替模式使用。三重 ADC 交替模式是针对同一通道的使用三个 ADC 交叉采集,就是在 ADC1 采样完等几个时钟周期后 ADC2 开始采样,此时 ADC1 处在转换 阶段,当 ADC2 采样完成再等几个时钟周期后 ADC3 就进行采样此时 ADC1 和 ADC2 处在 转换阶段,如果 ADC3 采样完成并且 ADC1 已经转换完成那么就可以准备下一轮的循环, 这样充分利用转换阶段时间达到增快采样速度的效果。AD 转换过程见图 30-6,利用 ADC 的转换阶段时间另外一个 ADC 进行采样,而不用像独立模式必须等待采样和转换结束后 才进行下一次采样及转换。 第 456 页 共 928 零死角玩转 STM32—F429 图 30-6 三重 ADC 交叉模式 30.6.1 硬件设计 三重 ADC 交叉模式是针对同一个通道的 ADC 采集模式,这种情况跟 30.4 30.4 小节的 单通道实验非常类似,只是同时使用三个 ADC 对同一通道进行采集,所以电路设计与之 相同即可,具体可参考图 30-5。 30.6.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。 跟单通道例程一样,我们编写两个 ADC 驱动文件,bsp_adc.h 和 bsp_adc.c,用来存放 ADC 所用 IO 引脚的初始化函数以及 ADC 配置相关函数,实际上这两个文件跟单通道实验 的文件非常相似。 1. 编程要点 1) 初始化配置 ADC 目标引脚为模拟输入模式; 2) 使能 ADC1、ADC2、ADC3 以及 DMA 时钟; 3) 配置 DMA 控制将 ADC 通用规矩数据寄存器数据转存到指定存储区; 4) 配置通用 ADC 为三重 ADC 交替模式,采样 4 分频,使用 DMA 模式 2; 5) 设置 ADC1、ADC2 和 ADC3 为 12 位分辨率,禁用扫描,连续转换,不需要外部 触发; 6) 设置 ADC1、ADC2 和 ADC3 转换通道顺序及采样时间; 第 457 页 共 928 零死角玩转 STM32—F429 7) 使能 ADC1 的 DMA 请求,在 ADC 转换完后自动请求 DMA 进行数据传输; 8) 启动 ADC1、ADC2 和 ADC3 转换; 9) 使能软件触发 ADC 转换。 ADC 转换结果数据使用 DMA 方式传输至指定的存储区,这样取代单通道实验使用中 断服务的读取方法。 2. 代码分析 ADC 宏定义 代码清单 30-11 多通道 ADC 相关宏定义 1 #define RHEOSTAT_ADC_CDR_ADDR ((uint32_t)0x40012308) 2 3 #define RHEOSTAT_ADC_GPIO_PORT GPIOC 4 #define RHEOSTAT_ADC_GPIO_PIN GPIO_Pin_3 5 #define RHEOSTAT_ADC_GPIO_CLK RCC_AHB1Periph_GPIOC 6 7 #define RHEOSTAT_ADC1 ADC1 8 #define RHEOSTAT_ADC1_CLK RCC_APB2Periph_ADC1 9 #define RHEOSTAT_ADC2 ADC2 10 #define RHEOSTAT_ADC2_CLK RCC_APB2Periph_ADC2 11 #define RHEOSTAT_ADC3 ADC3 12 #define RHEOSTAT_ADC3_CLK RCC_APB2Periph_ADC3 13 #define RHEOSTAT_ADC_CHANNEL ADC_Channel_13 14 15 #define RHEOSTAT_ADC_DMA_CLK RCC_AHB1Periph_DMA2 16 #define RHEOSTAT_ADC_DMA_CHANNEL DMA_Channel_0 17 #define RHEOSTAT_ADC_DMA_STREAM DMA2_Stream0 双重或者三重 ADC 需要使用通用规则数据寄存器 ADC_CDR,这点跟独立模式不同。 定义电位器动触点引脚作为三重 ADC 的模拟输入。 ADC GPIO 初始化函数 代码清单 30-12 ADC GPIO 初始化 1 static void Rheostat_ADC_GPIO_Config(void) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 5 // 使能 GPIO 时钟 6 RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_GPIO_CLK, ENABLE); 7 8 // 配置 IO 9 GPIO_InitStructure.GPIO_Pin = RHEOSTAT_ADC_GPIO_PIN; 10 // 配置为模拟输入 11 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; 12 // 不上拉不下拉 13 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL ; 14 GPIO_Init(RHEOSTAT_ADC_GPIO_PORT, &GPIO_InitStructure); 15 } 第 458 页 共 928 零死角玩转 STM32—F429 使用到 GPIO 时候都必须开启对应的 GPIO 时钟,GPIO 用于 AD 转换功能必须配置为 模拟输入模式。 配置三重 ADC 交替模式 代码清单 30-13 三重 ADC 交替模式配置 1 static void Rheostat_ADC_Mode_Config(void) 2{ 3 DMA_InitTypeDef DMA_InitStructure; 4 ADC_InitTypeDef ADC_InitStructure; 5 ADC_CommonInitTypeDef ADC_CommonInitStructure; 6 7 // 开启 ADC 时钟 8 RCC_APB2PeriphClockCmd(RHEOSTAT_ADC1_CLK , ENABLE); 9 RCC_APB2PeriphClockCmd(RHEOSTAT_ADC2_CLK , ENABLE); 10 RCC_APB2PeriphClockCmd(RHEOSTAT_ADC3_CLK , ENABLE); 11 12 // ------------------DMA Init 结构体参数 初始化----------------------- 13 // ADC1 使用 DMA2,数据流 0,通道 0,这个是手册固定死的 14 // 开启 DMA 时钟 15 RCC_AHB1PeriphClockCmd(RHEOSTAT_ADC_DMA_CLK, ENABLE); 16 // 外设基址为:ADC 数据寄存器地址 17 DMA_InitStructure.DMA_PeripheralBaseAddr = RHEOSTAT_ADC_CDR_ADDR; 18 // 存储器地址,实际上就是一个内部 SRAM 的变量 19 DMA_InitStructure.DMA_Memory0BaseAddr = (u32)ADC_ConvertedValue; 20 // 数据传输方向为外设到存储器 21 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; 22 // 缓冲区大小为 3,缓冲区的大小应该等于存储器的大小 23 DMA_InitStructure.DMA_BufferSize = 3; 24 // 外设寄存器只有一个,地址不用递增 25 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 26 // 存储器地址自动递增 27 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; 28 // // 外设数据大小为字,即四个字节 29 DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Word; 30 // 存储器数据大小也为字,跟外设数据大小相同 31 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; 32 // 循环传输模式 33 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; 34 // DMA 传输通道优先级为高,当使用一个 DMA 通道时,优先级设置不影响 35 DMA_InitStructure.DMA_Priority = DMA_Priority_High; 36 // 禁止 DMA FIFO ,使用直连模式 37 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; 38 // FIFO 大小,FIFO 模式禁止时,这个不用配置 39 DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; 40 DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; 41 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; 42 // 选择 DMA 通道,通道存在于流中 43 DMA_InitStructure.DMA_Channel = RHEOSTAT_ADC_DMA_CHANNEL; 44 //初始化 DMA 流,流相当于一个大的管道,管道里面有很多通道 45 DMA_Init(RHEOSTAT_ADC_DMA_STREAM, &DMA_InitStructure); 46 // 使能 DMA 流 47 DMA_Cmd(RHEOSTAT_ADC_DMA_STREAM, ENABLE); 48 49 // -------------------ADC Common 结构体 参数 初始化-------------------- 50 // 三重 ADC 交替模式 51 ADC_CommonInitStructure.ADC_Mode = ADC_TripleMode_Interl; 52 // 时钟为 fpclk2 4 分频 53 ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; 54 // 禁止 DMA 直接访问模式 55 ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_2; 第 459 页 共 928 零死角玩转 STM32—F429 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 } // 采样时间间隔 ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_10Cycles; ADC_CommonInit(&ADC_CommonInitStructure); // -------------------ADC Init 结构体 参数 初始化---------------------// ADC 分辨率 ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 禁止扫描模式,多通道采集才需要 ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 连续转换 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //禁止外部边沿触发 ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None; //使用软件触发,外部触发不用配置,注释掉即可 //ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_T1_CC1; //数据右对齐 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //转换通道 1 个 ADC_InitStructure.ADC_NbrOfConversion = 1; ADC_Init(RHEOSTAT_ADC1, &ADC_InitStructure); // 配置 ADC 通道转换顺序为 1,第一个转换,采样时间为 3 个时钟周期 ADC_RegularChannelConfig(RHEOSTAT_ADC1, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_3Cycles); //-----------------------------------------------------------------ADC_Init(RHEOSTAT_ADC2, &ADC_InitStructure); // 配置 ADC 通道转换顺序为 1,第一个转换,采样时间为 3 个时钟周期 ADC_RegularChannelConfig(RHEOSTAT_ADC2, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_3Cycles); //-----------------------------------------------------------------ADC_Init(RHEOSTAT_ADC3, &ADC_InitStructure); // 配置 ADC 通道转换顺序为 1,第一个转换,采样时间为 3 个时钟周期 ADC_RegularChannelConfig(RHEOSTAT_ADC3, RHEOSTAT_ADC_CHANNEL, 1, ADC_SampleTime_3Cycles); // 使能 DMA 请求 after last transfer (multi-ADC mode) ADC_MultiModeDMARequestAfterLastTransferCmd(ENABLE); // 使能 ADC DMA ADC_DMACmd(RHEOSTAT_ADC1, ENABLE); // 使能 ADC ADC_Cmd(RHEOSTAT_ADC1, ENABLE); ADC_Cmd(RHEOSTAT_ADC2, ENABLE); ADC_Cmd(RHEOSTAT_ADC3, ENABLE); //开始 adc 转换,软件触发 ADC_SoftwareStartConv(RHEOSTAT_ADC1); ADC_SoftwareStartConv(RHEOSTAT_ADC2); ADC_SoftwareStartConv(RHEOSTAT_ADC3); 首先,我们使用了 DMA_InitTypeDef 定义了一个 DMA 初始化类型变量,该结构体内 容 我 们 在 DMA 篇 已 经 做 了 非 常 详 细 的 讲 解 ; 另 外 还 使 用 ADC_InitTypeDef 和 ADC_CommonInitTypeDef 结构体分别定义一个 ADC 初始化和 ADC 通用类型变量,这两 个结构体我们之前已经有详细讲解。 调用 RCC_APB2PeriphClockCmd()开启 ADC 时钟以及 RCC_AHB1PeriphClockCmd()开 启 DMA 时钟。 我们需要对 DMA 进行必要的配置。首先设置外设基地址就是 ADC 的通用规则数据寄 存器地址;存储器的地址就是我们指定的数据存储区空间,ADC_ConvertedValue 是我们定 义的一个全局数组名,它是一个无符号 32 位有三个元素的整数数字;ADC 规则转换对应 第 460 页 共 928 零死角玩转 STM32—F429 只有一个数据寄存器所以地址不能递增,我们指定的存储区也需要递增地址。ADC 的通用 规则数据寄存器是 32 位有效,我们配置 ADC 为 DMA 模式 2,设置数据大小为字大小。 ADC 配置为连续转换模式 DMA 也设置为循环传输模式。设置好 DMA 相关参数后就使能 DMA 的 ADC 通道。 接下来我们使用 ADC_CommonInitTypeDef 结构体变量 ADC_CommonInitStructure 来 配置 ADC 为三重 ADC 交替模式、分频系数为 4、需要设置 DMA 模式 2、10 个周期的采 样延迟,并调用 ADC_CommonInit 函数完成 ADC 通用工作环境配置。 我们使用 ADC_InitTypeDef 结构体变量 ADC_InitStructure 来配置 ADC1 为 12 位分辨 率、不使用扫描模式、启动连续转换、使用内部软件触发无需外部触发事件、使用右对齐 数据格式、转换通道为 1,并调用 ADC_Init 函数完成 ADC1 工作环境配置。ADC2 和 ADC3 使用与 ADC1 相同配置即可。 ADC_RegularChannelConfig 函数用来绑定 ADC 通道转换顺序和采样时间。绑定 ADC 通道引脚并设置相应的转换顺序。 ADC_MultiModeDMARequestAfterLastTransferCmd 函数控制是否使能 ADC 的 DMA 请 求,如果使能请求,并调用 ADC_DMACmd 函数使能 DMA,则在 ADC 转换完成后就请求 DMA 实现数据传输。三重模式只需使能 ADC1 的 DMA 通道。 ADC_Cmd 函数控制 ADC 转换启动和停止。 最后,如果使用软件触发需要调用 ADC_SoftwareStartConvCmd 函数进行使能配置。 主函数 代码清单 30-14 主函数 1 int main(void) 2{ 3 /*初始化 USART1*/ 4 Debug_USART_Config(); 5 6 /* 7 初始化滑动变阻器用到的 DAC,ADC 数据采集完成后直接由 DMA 运输数据到 8 ADC_ConvertedValue 变量 DMA 直接改变 ADC_ConvertedValue 的值*/ 9 Rheostat_Init(); 10 11 printf("\r\n ----这是一个 ADC 实验(DMA 传输)----\r\n"); 12 13 while (1) { 14 Delay(0xffffee); 15 ADC_ConvertedValueLocal[0] = 16 (float)((uint16_t)ADC_ConvertedValue[0]*3.3/4096); 17 ADC_ConvertedValueLocal[1] = 18 (float)((uint16_t)ADC_ConvertedValue[2]*3.3/4096); 19 ADC_ConvertedValueLocal[2] = 20 (float)((uint16_t)ADC_ConvertedValue[1]*3.3/4096); 21 printf("\r\n The current AD value = 0x%08X \r\n", 22 ADC_ConvertedValue[0]); 23 printf("\r\n The current AD value = 0x%08X \r\n", 24 ADC_ConvertedValue[1]); 25 printf("\r\n The current AD value = 0x%08X \r\n", 26 ADC_ConvertedValue[2]); 27 printf("\r\n The current ADC1 value = %f V \r\n", 28 ADC_ConvertedValueLocal[0]); 29 printf("\r\n The current ADC2 value = %f V \r\n", 第 461 页 共 928 零死角玩转 STM32—F429 30 ADC_ConvertedValueLocal[1]); 31 printf("\r\n The current ADC3 value = %f V \r\n", 32 ADC_ConvertedValueLocal[2]); 33 } 34 } 主函数先调用 USARTx_Config 函数配置调试串口相关参数,函数定义在 bsp_debug_usart.c 文件中。 接下来调用 Rheostat_Init 函数进行 ADC 初始化配置并启动 ADC。Rheostat_Init 函数是 定义在 bsp_adc.c 文件中,它只是简单的分别调用 Rheostat_ADC_GPIO_Config()和 Rheostat_ADC_Mode_Config ()。 Delay 函数只是一个简单的延时函数。 我们配置了 DMA 数据传输所以它会自动把 ADC 转换完成后数据保存到数组变量 ADC_ConvertedValue 内,根据 DMA 模式 2 的数据存放规则,ADC_ConvertedValue[0]的低 16 位存放 ADC1 数据、高 16 位存放 ADC2 数据,ADC_ConvertedValue[1]的低 16 位存放 ADC3 数据、高 16 位存放 ADC1 数据,ADC_ConvertedValue[2]的低 16 位存放 ADC2 数据、 高 16 位存放 ADC3 数据,我们可以根据需要提取出对应 ADC 的转换结果数据。经过简单 地计算就可以得到每个 ADC 对应的实际电压。 最后就是把相关数据打印至串口调试助手。 30.6.3 下载验证 保证开发板相关硬件连接正确,用 USB 线连接开发板“USB TO UART”接口跟电脑, 在电脑端打开串口调试助手,把编译好的程序下载到开发板。在串口调试助手可看到不断 有数据从开发板传输过来,此时我们旋转电位器改变其电阻值,那么对应的数据也会有变 化。 30.7 每课一问 1、如果设置 ADC 分辨率为 6、8 或者 10 位时,输入电压值如何计算? 2、根据独立模式单通道采集实验设计实现相同功能,要求使用外部硬件触发转换方式。 3、编写一个双重 ADC 交替模式程序,实现类似三重 ADC 交替模式实验功能,要求 使用 DMA 模式 2 实现程序功能。 第 462 页 共 928 零死角玩转 STM32—F429 第31章 TIM—基本定时器 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 学习本章时,配合《STM32F4xx 参考手册》基本定时器章节一起阅读,效果会更佳, 特别是涉及到寄存器说明的部分。 特别说明,本书内容是以 STM32F42x 系列控制器资源讲解。 31.1 TIM 简介 定时器(Timer)最基本的功能就是定时了,比如定时发送 USART 数据、定时采集 AD 数据等等。如果把定时器与 GPIO 结合起来使用的话可以实现非常丰富的功能,可以测量 输入信号的脉冲宽度,可以生产输出波形。定时器生产 PWM 控制电机状态是工业控制普 遍方法,这方面知识非常有必要深入了解。 STM32F42xxx 系列控制器有 2 个高级控制定时器、10 个通用定时器和 2 个基本定时器, 还有 2 个看门狗定时器。看门狗定时器不在本章讨论范围,有专门讲解的章节。控制器上 所有定时器都是彼此独立的,不共享任何资源。各个定时器特性参考表 31-1。 表 31-1 各个定时器特性 定时器类 型 Timer 计数器 分辨率 计数器类 型 预分频系数 DMA 请 求生成 捕获/ 比较 通道 互补 最大接口时 最大定时器 输出 钟(MHz) 时钟(MHz) 高级控制 TIM1 和 TIM8 16 位 TIM2, TIM5 32 位 TIM3, TIM4 16 位 通用 TIM9 TIM10, TIM11 16 位 16 位 递增、递 减、递增/ 递减 1~65536(整 数) 有 递增、递 减、递增/ 递减 1~65536(整 数) 有 递增、递 减、递增/ 递减 1~65536(整 数) 有 递增 1~65536(整 无 数) 递增 1~65536(整 无 数) 4 有 90 180 (APB2) 4 无 45 90/180 (APB1) 4 无 45 90/180 (APB1) 2 无 90 180 (APB2) 1 无 90 180 (APB2) TIM12 16 位 递增 1~65536(整 无 数) 2 无 45 90/180 (APB1) TIM13, TIM14 16 位 递增 1~65536(整 无 数) 1 无 45 90/180 (APB1) 基本 TIM6 和 TIM7 16 位 递增 1~65536(整 有 数) 0 无 45 90/180 (APB1) 其中最大定时器时钟可通过 RCC_DCKCFGR 寄存器配置为 90MHz 或者 180MHz。 第 463 页 共 928 零死角玩转 STM32—F429 定时器功能强大,这一点透过《STM32F4xx 中文参考手册》讲解定时器内容就有 160 多页就显而易见了。定时器篇幅长,内容多,对于新手想完全掌握确实有些难度,特别参 考手册是先介绍高级控制定时器,然后介绍通用定时器,最后才介绍基本定时器。实际上, 就功能上来说通用定时器包含所有基本定时器功能,而高级控制定时器包含通用定时器所 有功能。所以高级控制定时器功能繁多,但也是最难理解的,本章我们先选择最简单的基 本定时器进行讲解。 31.2 基本定时器 基本定时器比高级控制定时器和通用定时器功能少,结构简单,理解起来更容易,我 们就开始先讲解基本定时器内容。基本定时器主要两个功能,第一就是基本定时功能,生 成时基,第二就是专门用于驱动数模转换器(DAC)。关于驱动 DAC 具体应用参考 DAC 章 节。 控制器有两个基本定时器 TIM6 和 TIM7,功能完全一样,但所用资源彼此都完全独立, 可以同时使用。在本章内容中,以 TIMx 统称基本定时器。 基本上定时器 TIM6 和 TIM7 是一个 16 位向上递增的定时器,当我在自动重载寄存器 (TIMx_ARR)添加一个计数值后并使能 TIMx,计数寄存器(TIMx_CNT)就会从 0 开始递增, 当 TIMx_CNT 的数值与 TIMx_ARR 值相同时就会生成事件并把 TIMx_CNT 寄存器清 0, 完成一次循环过程。如果没有停止定时器就循环执行上述过程。这些只是大概的流程,希 望大家有个感性认识,下面细讲整个过程。 31.3 基本定时器功能框图 基本定时器的功能框图包含了基本定时器最核心内容,掌握了功能框图,对基本定时 器就有一个整体的把握,在编程时思路就非常清晰,见图 31-1。 首先先看图 31-1 中绿色框内容,第一个是带有阴影的方框,方框内容一般是一个寄存 器名称,比如图中主体部分的自动重载寄存器(TIMx_ARR)或 PSC 预分频器(TIMx_PSC), 这里要特别突出的是阴影这个标志的作用,它表示这个寄存器还自带有影子寄存器,在硬 件结构上实际是有两个寄存器,源寄存器是我们可以进行读写操作,而影子寄存器我们是 完全无法操作的,有内部硬件使用。影子寄存器是在程序运行时真正起到作用的,源寄存 器只是给我们读写用的,只有在特定时候(特定事件发生时)才把源寄存器的值拷贝给它的 影子寄存器。多个影子寄存器一起使用可以到达同步更新多个寄存器内容的目的。 接下来是一个指向右下角的图标,它表示一个事件,而一个指向右上角的图标表示中 断和 DMA 输出。这个我们把它放在图中主体更好理解。图中的自动重载寄存器有影子寄 存器,它左边有一个带有“U”字母的事件图标,表示在更新事件生成时就把自动重载寄 存器内容拷贝到影子寄存器内,这个与上面分析是一致。寄存器右边的事件图标、中断和 DMA 输出图标表示在自动重载寄存器值与计数器寄存器值相等时生成事件、中断和 DMA 输出。 第 464 页 共 928 零死角玩转 STM32—F429 图 31-1 基本定时器功能框图 1. ①时钟源 定时器要实现计数必须有个时钟源,基本定时器时钟只能来自内部时钟,高级控制定 时器和通用定时器还可以选择外部时钟源或者直接来自其他定时器等待模式。我们可以通 过 RCC 专用时钟配置寄存器(RCC_DCKCFGR)的 TIMPRE 位设置所有定时器的时钟频率, 我们一般设置该位为默认值 0,使得表 31-1 中可选的最大定时器时钟为 90MHz,即基本定 时器的内部时钟(CK_INT)频率为 90MHz。 基本定时器只能使用内部时钟,当 TIM6 和 TIM7 控制寄存器 1(TIMx_CR1)的 CEN 位 置 1 时,启动基本定时器,并且预分频器的时钟来源就是 CK_INT。对于高级控制定时器 和通用定时器的时钟源可以来找控制器外部时钟、其他定时器等等模式,较为复杂,我们 在相关章节会详细介绍。 2. ②控制器 定时器控制器控制实现定时器功能,控制定时器复位、使能、计数是其基础功能,基 本定时器还专门用于 DAC 转换触发。 第 465 页 共 928 零死角玩转 STM32—F429 3. ③计数器 基本定时器计数过程主要涉及到三个寄存器内容,分别是计数器寄存器(TIMx_CNT)、 预分频器寄存器(TIMx_PSC)、自动重载寄存器(TIMx_ARR),这三个寄存器都是 16 位有效 数字,即可设置值为 0 至 65535。 首先我们来看图 31-1 中预分频器 PSC,它有一个输入时钟 CK_PSC 和一个输出时钟 CK_CNT。输入时钟 CK_PSC 来源于控制器部分,基本定时器只有内部时钟源所以 CK_PSC 实际等于 CK_INT,即 90MHz。在不同应用场所,经常需要不同的定时频率,通 过设置预分频器 PSC 的值可以非常方便得到不同的 CK_CNT,实际计算为:fCK_CNT 等 于 fCK_PSC/(PSC[15:0]+1)。 图 31-2 是将预分频器 PSC 的值从 1 改为 4 时计数器时钟变化过程。原来是 1 分频, CK_PSC 和 CK_CNT 频率相同。向 TIMx_PSC 寄存器写入新值时,并不会马上更新 CK_CNT 输出频率,而是等到更新事件发生时,把 TIMx_PSC 寄存器值更新到影子寄存器 中,使其真正产生效果。更新为 4 分频后,在 CK_PSC 连续出现 4 个脉冲后 CK_CNT 才产 生一个脉冲。 图 31-2 基本定时器时钟源分频 在定时器使能(CEN 置 1)时,计数器 COUNTER 根据 CK_CNT 频率向上计数,即每来 一个 CK_CNT 脉冲,TIMx_CNT 值就加 1。当 TIMx_CNT 值与 TIMx_ARR 的设定值相等 时就自动生成事件并 TIMx_CNT 自动清零,然后自动重新开始计数,如此重复以上过程。 为此可见,我们只要设置 CK_PSC 和 TIMx_ARR 这两个寄存器的值就可以控制事件生成的 时间,而我们一般的应用程序就是在事件生成的回调函数中运行的。在 TIMx_CNT 递增至 与 TIMx_ARR 值相等,我们叫做为定时器上溢。 自动重载寄存器 TIMx_ARR 用来存放于计数器值比较的数值,如果两个数值相等就生 成事件,将相关事件标志位置位,生成 DMA 和中断输出。TIMx_ARR 有影子寄存器,可 以通过 TIMx_CR1 寄存器的 ARPE 位控制影子寄存器功能,如果 ARPE 位置 1,影子寄存 第 466 页 共 928 零死角玩转 STM32—F429 器有效,只有在事件更新时才把 TIMx_ARR 值赋给影子寄存器。如果 ARPE 位为 0,修改 TIMx_ARR 值马上有效。 4. 定时器周期计算 经过上面分析,我们知道定时事件生成时间主要由 TIMx_PSC 和 TIMx_ARR 两个寄存 器值决定,这个也就是定时器的周期。比如我们需要一个 1s 周期的定时器,具体这两个寄 存器值该如何设置内。假设,我们先设置 TIMx_ARR 寄存器值为 9999,即当 TIMx_CNT 从 0 开始计算,刚好等于 9999 时生成事件,总共计数 10000 次,那么如果此时时钟源周期 为 100us 即可得到刚好 1s 的定时周期。 接下来问题就是设置 TIMx_PSC 寄存器值使得 CK_CNT 输出为 100us 周期(10000Hz) 的时钟。预分频器的输入时钟 CK_PSC 为 90MHz,所以设置预分频器值为(9000-1)即可满 足。 31.4 定时器初始化结构体详解 标准库函数对定时器外设建立了四个初始化结构体,基本定时器只用到其中一个即 TIM_TimeBaseInitTypeDef,该结构体成员用于设置定时器基本工作参数,并由定时器基本 初始化配置函数 TIM_TimeBaseInit 调用,这些设定参数将会设置定时器相应的寄存器,达 到配置定时器工作环境的目的。这一章我们只介绍 TIM_TimeBaseInitTypeDef 结构体,其 他结构体将在相关章节介绍。 初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个 成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 stm32f4xx_tim.h 文件中, 初始化库函数定义在 stm32f4xx_tim.c 文件中,编程时我们可以结合这两个文件内注释使用。 代码清单 31-1 定时器基本初始化结构体 1 typedef struct { 2 uint16_t TIM_Prescaler; 3 uint16_t TIM_CounterMode; 4 uint32_t TIM_Period; 5 uint16_t TIM_ClockDivision; 6 uint8_t TIM_RepetitionCounter; 7 } TIM_TimeBaseInitTypeDef; // 预分频器 // 计数模式 // 定时器周期 // 时钟分频 // 重复计算器 (1) TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器时钟,它设定 TIMx_PSC 寄存器的值。可设置范围为 0 至 65535,实现 1 至 65536 分频。 (2) TIM_CounterMode:定时器计数方式,可是在为向上计数、向下计数以及三种中心对 齐模式。基本定时器只能是向上计数,即 TIMx_CNT 只能从 0 开始递增,并且无需初 始化。 (3) TIM_Period:定时器周期,实际就是设定自动重载寄存器的值,在事件生成时更新到 影子寄存器。可设置范围为 0 至 65535。 (4) TIM_ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与数字滤波器采样时钟 频率分频比,基本定时器没有此功能,不用设置。 (5) TIM_RepetitionCounter:重复计数器,属于高级控制寄存器专用寄存器位,利用它可 以非常容易控制输出 PWM 的个数。这里不用设置。 第 467 页 共 928 零死角玩转 STM32—F429 虽然定时器基本初始化结构体有 5 个成员,但对于基本定时器只需设置其中两个就可 以,想想使用基本定时器就是简单。 31.5 基本定时器定时实验 在 DAC 转换中几乎都用到基本定时器,使用有关基本定时器触发 DAC 转换内容在 DAC 章节讲解即可,这里就利用基本定时器实现简单的定时功能。 我们使用基本定时器循环定时 0.5s 并使能定时器中断,每到 0.5s 就在定时器中断服务 函数翻转 RGB 彩灯,使得最终效果 RGB 彩灯暗 0.5s,亮 0.5s,如此循环。 31.5.1 硬件设计 基本定时器没有相关 GPIO,这里我们只用定时器的定时功能,无效其他外部引脚, 至于 RGB 彩灯硬件可参考 GPIO 章节。 31.5.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_basic_tim.c 和 bsp_basic_tim.h 文件用来存基本定时器驱动程序及相关宏定义,中断服务函数放在 stm32f4xx_it.h 文件中。 1. 编程要点 (1) 初始化 RGB 彩灯 GPIO; (2) 开启基本定时器时钟; (3) 设置定时器周期和预分频器; (4) 启动定时器更新中断,并开启定时器; (5) 定时器中断服务函数实现 RGB 彩灯翻转。 2. 软件分析 宏定义 代码清单 31-2 宏定义 1 #define BASIC_TIM 2 #define BASIC_TIM_CLK 3 4 #define BASIC_TIM_IRQn 5 #define BASIC_TIM_IRQHandler TIM6 RCC_APB1Periph_TIM6 TIM6_DAC_IRQn TIM6_DAC_IRQHandler 使用宏定义非常方便程序升级、移植。 NCIV 配置 代码清单 31-3 NVIC 配置 1 static void TIMx_NVIC_Configuration(void) 2{ 3 NVIC_InitTypeDef NVIC_InitStructure; 4 // 设置中断组为 0 5 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); 第 468 页 共 928 零死角玩转 STM32—F429 6 7 8 9 10 11 12 13 14 } // 设置中断来源 NVIC_InitStructure.NVIC_IRQChannel = BASIC_TIM_IRQn; // 设置抢占优先级 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 设置子优先级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); 实验用到定时器更新中断,需要配置 NVIC,实验只有一个中断,对 NVIC 配置没什 么具体要求。 基本定时器模式配置 代码清单 31-4 基本定时器模式配置 1 static void TIM_Mode_Config(void) 2{ 3 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 4 5 // 开启 TIMx_CLK,x[6,7] 6 RCC_APB1PeriphClockCmd(BASIC_TIM_CLK, ENABLE); 7 8 /* 累计 TIM_Period 个后产生一个更新或者中断*/ 9 //当定时器从 0 计数到 4999,即为 5000 次,为一个定时周期 10 TIM_TimeBaseStructure.TIM_Period = 5000-1; 11 12 //定时器时钟源 TIMxCLK = 2 * PCLK1 13 // PCLK1 = HCLK / 4 14 // => TIMxCLK=HCLK/2=SystemCoreClock/2=90MHz 15 // 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=10000Hz 16 TIM_TimeBaseStructure.TIM_Prescaler = 9000-1; 17 18 // 初始化定时器 TIMx, x[2,3,4,5] 19 TIM_TimeBaseInit(BASIC_TIM, &TIM_TimeBaseStructure); 20 21 22 // 清除定时器更新中断标志位 23 TIM_ClearFlag(BASIC_TIM, TIM_FLAG_Update); 24 25 // 开启定时器更新中断 26 TIM_ITConfig(BASIC_TIM,TIM_IT_Update,ENABLE); 27 28 // 使能定时器 29 TIM_Cmd(BASIC_TIM, ENABLE); 30 } 使用定时器之前都必须开启定时器时钟,基本定时器属于 APB1 总线外设。 接下来设置定时器周期数为 4999,即计数 5000 次生成事件。设置定时器预分频器为 (9000-1),基本定时器使能内部时钟,频率为 90MHz,经过预分频器后得到 10KHz 的频率。 然后就是调用 TIM_TimeBaseInit 函数完成定时器配置。 TIM_ClearFlag 函数用来在配置中断之前清除定时器更新中断标志位,实际是清零 TIMx_SR 寄存器的 UIF 位。 使用 TIM_ITConfig 函数配置使能定时器更新中断,即在发生上溢时产生中断。 最后使用 TIM_Cmd 函数开启定时器。 定时器中断服务函数 代码清单 31-5 定时器中断服务函数 1 void BASIC_TIM_IRQHandler (void) 第 469 页 共 928 零死角玩转 STM32—F429 2{ 3 if ( TIM_GetITStatus( BASIC_TIM, TIM_IT_Update) != RESET ) { 4 LED1_TOGGLE; 5 TIM_ClearITPendingBit(BASIC_TIM , TIM_IT_Update); 6 } 7} 我们在 TIM_Mode_Config 函数启动了定时器更新中断,在发生中断时,中断服务函数 就得到运行。在服务函数内我们先调用定时器中断标志读取函数 TIM_GetITStatus 获取当 前定时器中断位状态,确定产生中断后才运行 RGB 彩灯翻转动作,并使用定时器标志位清 除函数 TIM_ClearITPendingBit 清除中断标志位。 主函数 代码清单 31-6 主函数 1 int main(void) 2{ 3 4 LED_GPIO_Config(); 5 6 /* 初始化基本定时器定时,1s 产生一次中断 */ 7 TIMx_Configuration(); 8 9 while (1) { 10 } 11 } 实验用到 RGB 彩灯,需要对其初始化配置。LED_GPIO_Config 函数是定义在 bsp_led.c 文件的完成 RGB 彩灯 GPIO 初始化配置的程序。 TIMx_Configuration 函数是定义在 bsp_basic_tim.c 文件的一个函数,它只是简单的先 后调用 TIMx_NVIC_Configuration 和 TIM_Mode_Config 两个函数完成 NVIC 配置和基本定 时器模式配置。 31.5.3 下载验证 保证开发板相关硬件连接正确,把编译好的程序下载到开发板。开始 RGB 彩灯是暗的, 等一会 RGB 彩灯变为红色,再等一会又暗了,如此反复。如果我们使用表钟与 RGB 彩灯 闪烁对比,可以发现它是每 0.5s 改变一次 RGB 彩灯状态的。 31.6 每课一问 1. 计算基本定时器一次最长定时时间,如果需要使用基本定时器产生 100s 周期事件有什 么办法实现? 2. 修改实验程序,在保使其每 0.5s 翻转一次 LED1 的同时在每 10s 翻转 LED2。 第 470 页 共 928 零死角玩转 STM32—F429 第32章 TIM—高级定时器 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 学习本章时,配合《STM32F4xx 参考手册》高级定时器章节一起阅读,效果会更佳, 特别是涉及到寄存器说明的部分。 特别说明,本书内容是以 STM32F42x 系列控制器资源讲解。 上一章我们讲解了基本定时器功能,基本定时器功能简单,理解起来也容易。高级控 制定时器包含了通用定时器的功能,再加上已经有了基本定时器基础的基础,如果再把通 用定时器单独拿出来讲那内容有很多重复,实际效果不是很好,所以通用定时器不作为独 立章节讲解,可以在理解了高级定时器后参考《STM32F4xx 中文参考手册》通用定时器章 节内容理解即可。 32.1 高级控制定时器 高级控制定时器(TIM1 和 TIM8)和通用定时器在基本定时器的基础上引入了外部引脚, 可以输入捕获和输出比较功能。高级控制定时器比通用定时器增加了可编程死区互补输出、 重复计数器、带刹车(断路)功能,这些功能都是针对工业电机控制方面。这几个功能在本 书不做详细的介绍,主要介绍常用的输入捕获和输出比较功能。 高级控制定时器时基单元包含一个 16 位自动重载计数器 ARR,一个 16 位的计数器 CNT,可向上/下计数,一个 16 位可编程预分频器 PSC,预分频器时钟源有多种可选,有 内部的时钟、外部时钟。还有一个 8 位的重复计数器 RCR,这样最高可实现 40 位的可编 程定时。 STM32F429IGT6 的高级/通用定时器的 IO 分配具体见表 32-1。配套开发板因为 IO 资 源紧缺,定时器的 IO 很多已经复用它途,故下表中的 IO 只有部分可用于定时器的实验。 表 32-1 高级控制和通用定时器通道引脚分布 高级控制 TIM1 TIM8 TIM2 TIM5 TIM3 通用定时器 TIM4 TIM 9 TIM 10 TIM 11 TIM1 2 TIM 13 TIM 14 CH 1 PA8/PE9/ PC9 PC6/PI5 PA0/PA PA0/ PA6/PC PD12/ PE5/ PF6/ PF7/ PH6/ PF8/ PF9/ 5/PA15 PH10 6/PB4 PB6 PA2 PB8 PB9 PB14 PA6 PA7 CH PA7/PE8/ PA5/PA7 1N PB13 /PH13 CH PE11/PA 29 PC7/PI6 PA1/PB3 PA1/ PH11 PA7/PC 7/PB5 PD13/ PE6/ PB7 PA3 PH9/ PB15 CH PB0/PE1 PB0/PB1 2N 0/PB14 4/PH14 CH PE13/PA 3 10 PC8/PI7 PA2/PB1 PA2/ PB0/PC PD14/ 0 PH12 8 PB8 CH PB1/PE1 PB1/PB1 3N 2/PB15 5/PH15 第 471 页 共 928 零死角玩转 STM32—F429 CH PE14/PA 4 11 PC9/PI2 ET R PE7/PA1 2 PA0/PI3 BK PA6/PE1 IN 5/PB12 PA6/PI4 PA3/PB1 PA3/ 1 PI0 PA0/PA 5/PA15 PB1/PC PD15/ 9 PB9 PD2 PE0 32.2 高级控制定时器功能框图 高级控制定时器功能框图包含了高级控制定时器最核心内容,掌握了功能框图,对高 级控制定时器就有一个整体的把握,在编程时思路就非常清晰,见图 32-1。 关于图中带阴影的寄存器,即带有影子寄存器,指向左下角的事件更新图标以及指向 右上角的中断和 DMA 输出标志在上一章已经做了解释,这里就不再介绍。 图 32-1 高级控制定时器功能框图 1. ①时钟源 高级控制定时器有四个时钟源可选:  内部时钟源 CK_INT  外部时钟模式 1:外部输入引脚 TIx(x=1,2,3,4)  外部时钟模式 2:外部触发输入 ETR 第 472 页 共 928 零死角玩转 STM32—F429  内部触发输入 内部时钟源(CK_INT) 内部时钟 CK_INT 即来自于芯片内部,等于 180M,一般情况下,我们都是使用内部 时钟。当从模式控制寄存器 TIMx_SMCR 的 SMS 位等于 000 时,则使用内部时钟。 外部时钟模式 1 图 32-2 外部时钟模式 1 框图 ①:时钟信号输入引脚 当使用外部时钟模式 1 的时候,时钟信号来自于定时器的输入通道,总共有 4 个,分 别为 TI1/2/3/4,即 TIMx_CH1/2/3/4。具体使用哪一路信号,由 TIM_CCMx 的位 CCxS[1:0] 配置,其中 CCM1 控制 TI1/2,CCM2 控制 TI3/4。 ②:滤波器 如果来自外部的时钟信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用 滤波器对 ETRP 信号重新采样,来达到降频或者去除高频干扰的目的,具体的由 TIMx_CCMx 的位 ICxF[3:0]配置。 ③:边沿检测 边沿检测的信号来自于滤波器的输出,在成为触发信号之前,需要进行边沿检测,决 定是上升沿有效还是下降沿有效,具体的由 TIMx_CCER 的位 CCxP 和 CCxNP 配置。 ④:触发选择 当使用外部时钟模式 1 时,触发源有两个,一个是滤波后的定时器输入 1(TI1FP1) 和滤波后的定时器输入 2(TI2FP2),具体的由 TIMxSMCR 的位 TS[2:0]配置。 ⑤:从模式选择 选定了触发源信号后,最后我们需把信号连接到 TRGI 引脚,让触发信号成为外部时 钟模式 1 的输入,最终等于 CK_PSC,然后驱动计数器 CNT 计数。具体的配置 TIMx_SMCR 的位 SMS[2:0]为 000 即可选择外部时钟模式 1。 ⑥:使能计数器 经过上面的 5 个步骤之后,最后我们只需使能计数器开始计数,外部时钟模式 1 的配 置就算完成。使能计数器由 TIMx_CR1 的位 CEN 配置。 第 473 页 共 928 外部时钟模式 2 零死角玩转 STM32—F429 图 32-3 外部时钟模式 2 框图 ①:时钟信号输入引脚 当使用外部时钟模式 2 的时候,时钟信号来自于定时器的特定输入通道 TIMx_ETR, 只有 1 个。 ②:外部触发极性 来自 ETR 引脚输入的信号可以选择为上升沿或者下降沿有效,具体的由 TIMx_SMCR 的位 ETP 配置。 ③:外部触发预分频器 由于 ETRP 的信号的频率不能超过 TIMx_CLK(180M)的 1/4,当触发信号的频率很 高的情况下,就必须使用分频器来降频,具体的由 TIMx_SMCR 的位 ETPS[1:0]配置。 ④:滤波器 如果 ETRP 的信号的频率过高或者混杂有高频干扰信号的话,我们就需要使用滤波器 对 ETRP 信号重新采样,来达到降频或者去除高频干扰的目的。具体的由 TIMx_SMCR 的 位 ETF[3:0]配置,其中的 fDTS 是由内部时钟 CK_INT 分频得到,具体的由 TIMx_CR1 的位 CKD[1:0]配置。 ⑤:从模式选择 经过滤波器滤波的信号连接到 ETRF 引脚后,触发信号成为外部时钟模式 2 的输入, 最终等于 CK_PSC,然后驱动计数器 CNT 计数。具体的配置 TIMx_SMCR 的位 ECE 为 1 即可选择外部时钟模式 2。 ⑥:使能计数器 经过上面的 5 个步骤之后,最后我们只需使能计数器开始计数,外部时钟模式 2 的配 置就算完成。使能计数器由 TIMx_CR1 的位 CEN 配置。 内部触发输入 内部触发输入是使用一个定时器作为另一个定时器的预分频器。硬件上高级控制定时 器和通用定时器在内部连接在一起,可以实现定时器同步或级联。主模式的定时器可以对 从模式定时器执行复位、启动、停止或提供时钟。高级控制定时器和部分通用定时器 (TIM2 至 TIM5)可以设置为主模式或从模式,TIM9 和 TIM10 可设置为从模式。 第 474 页 共 928 零死角玩转 STM32—F429 图 32-4 为主模式定时器(TIM1)为从模式定时器(TIM2)提供时钟,即 TIM1 用作 TIM2 的预分频器。 图 32-4 TIM1 用作 TIM2 的预分频器 2. ②控制器 高级控制定时器控制器部分包括触发控制器、从模式控制器以及编码器接口。触发控 制器用来针对片内外设输出触发信号,比如为其它定时器提供时钟和触发 DAC/ADC 转换。 编码器接口专门针对编码器计数而设计。从模式控制器可以控制计数器复位、启动、递增/ 递减、计数。 3. ③时基单元 图 32-5 高级定时器时基单元 高级控制定时器时基单元包括四个寄存器,分别是计数器寄存器(CNT)、预分频器寄 存器(PSC)、自动重载寄存器(ARR)和重复计数器寄存器(RCR)。其中重复计数器 RCR 是高 级定时器独有,通用和基本定时器没有。前面三个寄存器都是 16 位有效,TIMx_RCR 寄存 器是 8 位有效。 预分频器 PSC 预分频器 PSC,有一个输入时钟 CK_PSC 和一个输出时钟 CK_CNT。输入时钟 CK_PSC 就是上面时钟源的输出,输出 CK_CNT 则用来驱动计数器 CNT 计数。通过设置 预分频器 PSC 的值可以得到不同的 CK_CNT,实际计算为:fCK_CNT 等于 fCK_PSC/(PSC[15:0]+1),可以实现 1 至 65536 分频。 第 475 页 共 928 零死角玩转 STM32—F429 计数器 CNT 高级控制定时器的计数器有三种计数模式,分别为递增计数模式、递减计数模式和递 增/递减(中心对齐)计数模式。 (1) 递增计数模式下,计数器从 0 开始计数,每来一个 CK_CNT 脉冲计数器就增加 1,直 到计数器的值与自动重载寄存器 ARR 值相等,然后计数器又从 0 开始计数并生成计数 器上溢事件,计数器总是如此循环计数。如果禁用重复计数器,在计数器生成上溢事 件就马上生成更新事件(UEV);如果使能重复计数器,每生成一次上溢事件重复计数 器内容就减 1,直到重复计数器内容为 0 时才会生成更新事件。 (2) 递减计数模式下,计数器从自动重载寄存器 ARR 值开始计数,每来一个 CK_CNT 脉 冲计数器就减 1,直到计数器值为 0,然后计数器又从自动重载寄存器 ARR 值开始递 减计数并生成计数器下溢事件,计数器总是如此循环计数。如果禁用重复计数器,在 计数器生成下溢事件就马上生成更新事件;如果使能重复计数器,每生成一次下溢事 件重复计数器内容就减 1,直到重复计数器内容为 0 时才会生成更新事件。 (3) 中心对齐模式下,计数器从 0 开始递增计数,直到计数值等于(ARR-1)值生成计数器上 溢事件,然后从 ARR 值开始递减计数直到 1 生成计数器下溢事件。然后又从 0 开始计 数,如此循环。每次发生计数器上溢和下溢事件都会生成更新事件。 自动重载寄存器 ARR 自动重载寄存器 ARR 用来存放与计数器 CNT 比较的值,如果两个值相等就递减重复 计数器。可以通过 TIMx_CR1 寄存器的 ARPE 位控制自动重载影子寄存器功能,如果 ARPE 位置 1,自动重载影子寄存器有效,只有在事件更新时才把 TIMx_ARR 值赋给影子 寄存器。如果 ARPE 位为 0,则修改 TIMx_ARR 值马上有效。 重复计数器 RCR 在基本/通用定时器发生上/下溢事件时直接就生成更新事件,但对于高级控制定时器 却不是这样,高级控制定时器在硬件结构上多出了重复计数器,在定时器发生上溢或下溢 事件是递减重复计数器的值,只有当重复计数器为 0 时才会生成更新事件。在发生 N+1 个 上溢或下溢事件(N 为 RCR 的值)时产生更新事件。 第 476 页 共 928 零死角玩转 STM32—F429 4. ④输入捕获 图 32-6 输入捕获功能框图 输入捕获可以对输入的信号的上升沿,下降沿或者双边沿进行捕获,常用的有测量输 入信号的脉宽和测量 PWM 输入信号的频率和占空比这两种。 输入捕获的大概的原理就是,当捕获到信号的跳变沿的时候,把计数器 CNT 的值锁存 到捕获寄存器 CCR 中,把前后两次捕获到的 CCR 寄存器中的值相减,就可以算出脉宽或 者频率。如果捕获的脉宽的时间长度超过你的捕获定时器的周期,就会发生溢出,这个我 们需要做额外的处理。 ①输入通道 需要被测量的信号从定时器的外部引脚 TIMx_CH1/2/3/4 进入,通常叫 TI1/2/3/4,在后 面的捕获讲解中对于要被测量的信号我们都以 TIx 为标准叫法。 ②输入滤波器和边沿检测器 当输入的信号存在高频干扰的时候,我们需要对输入信号进行滤波,即进行重新采样, 根据采样定律,采样的频率必须大于等于两倍的输入信号。比如输入的信号为 1M,又存 在高频的信号干扰,那么此时就很有必要进行滤波,我们可以设置采样频率为 2M,这样 可以在保证采样到有效信号的基础上把高于 2M 的高频干扰信号过滤掉。 滤波器的配置由 CR1 寄存器的位 CKD[1:0]和 CCMR1/2 的位 ICxF[3:0]控制。从 ICxF 位的描述可知,采样频率 fSAMPLE 可以由 fCK_INT 和 fDTS 分频后的时钟提供,其中是 fCK_INT 内 部时钟,fDTS 是 fCK_INT 经过分频后得到的频率,分频因子由 CKD[1:0]决定,可以是不分频, 2 分频或者是 4 分频。 边沿检测器用来设置信号在捕获的时候是什么边沿有效,可以是上升沿,下降沿,或 者是双边沿,具体的由 CCER 寄存器的位 CCxP 和 CCxNP 决定。 第 477 页 共 928 零死角玩转 STM32—F429 ③捕获通道 捕获通道就是图中的 IC1/2/3/4,每个捕获通道都有相对应的捕获寄存器 CCR1/2/3/4, 当发生捕获的时候,计数器 CNT 的值就会被锁存到捕获寄存器中。 这里我们要搞清楚输入通道和捕获通道的区别,输入通道是用来输入信号的,捕获通 道是用来捕获输入信号的通道,一个输入通道的信号可以同时输入给两个捕获通道。比如 输入通道 TI1 的信号经过滤波边沿检测器之后的 TI1FP1 和 TI1FP2 可以进入到捕获通道 IC1 和 IC2,其实这就是我们后面要讲的 PWM 输入捕获,只有一路输入信号(TI1)却占 用了两个捕获通道(IC1 和 IC2)。当只需要测量输入信号的脉宽时候,用一个捕获通道即 可。输入通道和捕获通道的映射关系具体由寄存器 CCMRx 的位 CCxS[1:0]配置。 ④的预分频器 ICx 的输出信号会经过一个预分频器,用于决定发生多少个事件时进行一次捕获。具 体的由寄存器 CCMRx 的位 ICxPSC 配置,如果希望捕获信号的每一个边沿,则不分频。 ⑤捕获寄存器 经过预分频器的信号 ICxPS 是最终被捕获的信号,当发生捕获时(第一次),计数器 CNT 的值会被锁存到捕获寄存器 CCR 中,还会产生 CCxI 中断,相应的中断位 CCxIF(在 SR 寄存器中)会被置位,通过软件或者读取 CCR 中的值可以将 CCxIF 清 0。如果发生第 二次捕获(即重复捕获:CCR 寄存器中已捕获到计数器值且 CCxIF 标志已置 1),则捕获 溢出标志位 CCxOF(在 SR 寄存器中)会被置位,CCxOF 只能通过软件清零。 5. ⑤输出比较 图 32-7 输出比较功能框图 第 478 页 共 928 零死角玩转 STM32—F429 输出比较就是通过定时器的外部引脚对外输出控制信号,有冻结、将通道 X (x=1,2,3,4)设置为匹配时输出有效电平、将通道 X 设置为匹配时输出无效电平、翻转、 强制变为无效电平、强制变为有效电平、PWM1 和 PWM2 这八种模式,具体使用哪种模式 由寄存器 CCMRx 的位 OCxM[2:0]配置。其中 PWM 模式是输出比较中的特例,使用的也 最多。 ①比较寄存器 当计数器 CNT 的值跟比较寄存器 CCR 的值相等的时候,输出参考信号 OCxREF 的信 号的极性就会改变,其中 OCxREF=1(高电平)称之为有效电平,OCxREF=0(低电平) 称之为无效电平,并且会产生比较中断 CCxI,相应的标志位 CCxIF(SR 寄存器中)会置 位。然后 OCxREF 再经过一系列的控制之后就成为真正的输出信号 OCx/OCxN。 ②死区发生器 在生成的参考波形 OCxREF 的基础上,可以插入死区时间,用于生成两路互补的输出 信号 OCx 和 OCxN,死区时间的大小具体由 BDTR 寄存器的位 DTG[7:0]配置。死区时间 的大小必须根据与输出信号相连接的器件及其特性来调整。下面我们简单举例说明下带死区的 PWM 信号的应用,我们以一个板桥驱动电路为例。 图 32-8 半桥驱动电路 在这个半桥驱动电路中,Q1 导通,Q2 截止,此时我想让 Q1 截止 Q2 导通,肯定是要先让 Q1 截止一段时间之后,再等一段时间才让 Q2 导通,那么这段等待的时间就称为死区时间,因 为 Q1 关闭需要时间(由 MOS 管的工艺决定)。如果 Q1 关闭之后,马上打开 Q2,那么此时一 段时间内相当于 Q1 和 Q2 都导通了,这样电路会短路。 图 32-9 是针对上面的半桥驱动电路而画的带死区插入的 PWM 信号,图中的死区时间要根 据 MOS 管的工艺来调节。 第 479 页 共 928 零死角玩转 STM32—F429 ③输出控制 图 32-9 带死区插入的互补输出 图 32-10 输出比较(通道 1~3)的输出控制框图 在输出比较的输出控制中,参考信号 OCxREF 在经过死区发生器之后会产生两路带死 区的互补信号 OCx_DT 和 OCxN_DT(通道 1~3 才有互补信号,通道 4 没有,其余跟通道 1~3 一样),这两路带死区的互补信号然后就进入输出控制电路,如果没有加入死区控制, 那么进入输出控制电路的信号就直接是 OCxREF。 进入输出控制电路的信号会被分成两路,一路是原始信号,一路是被反向的信号,具 体的由寄存器 CCER 的位 CCxP 和 CCxNP 控制。经过极性选择的信号是否由 OCx 引脚输 出到外部引脚 CHx/CHxN 则由寄存器 CCER 的位 CxE/CxNE 配置。 如果加入了断路(刹车)功能,则断路和死区寄存器 BDTR 的 MOE、OSSI 和 OSSR 这三个位会共同影响输出的信号。 ④输出引脚 输出比较的输出信号最终是通过定时器的外部 IO 来输出的,分别为 CH1/2/3/4,其中 前面三个通道还有互补的输出通道 CH1/2/3N。更加详细的 IO 说明还请查阅相关的数据手 册。 第 480 页 共 928 零死角玩转 STM32—F429 6. ⑥断路功能 断路功能就是电机控制的刹车功能,使能断路功能时,根据相关控制位状态修改输出 信号电平。在任何情况下,OCx 和 OCxN 输出都不能同时为有效电平,这关系到电机控制 常用的 H 桥电路结构原因。 断路源可以是时钟故障事件,由内部复位时钟控制器中的时钟安全系统(CSS)生成,也 可以是外部断路输入 IO,两者是或运算关系。 系统复位启动都默认关闭断路功能,将断路和死区寄存器(TIMx_BDTR)的 BKE 为置 1, 使能断路功能。可通过 TIMx_BDTR 寄存器的 BKP 位设置设置断路输入引脚的有效电平, 设置为 1 时输入 BRK 为高电平有效,否则低电平有效。 发送断路时,将产生以下效果:  TIMx_BDTR 寄存器中主输出模式使能(MOE)位被清零,输出处于无效、空闲或 复位状态;  根据相关控制位状态控制输出通道引脚电平;当使能通道互补输出时,会根据情 况自动控制输出通道电平;  将 TIMx_SR 寄存器中的 BIF 位置 1,并可产生中断和 DMA 传输请求。  如果 TIMx_BDTR 寄存器中的 自动输出使能(AOE)位置 1,则 MOE 位会在发生下 一个 UEV 事件时自动再次置 1。 32.3 输入捕获应用 输入捕获一般应用在两个方面,一个方面是脉冲跳变沿时间测量,另一方面是 PWM 输入测量。 32.3.1 测量脉宽或者频率 图 32-11 脉宽/频率测量示意图 1. 测量频率 当捕获通道 TIx 上出现上升沿时,发生第一次捕获,计数器 CNT 的值会被锁存到捕获 寄存器 CCR 中,而且还会进入捕获中断,在中断服务程序中记录一次捕获(可以用一个标 志变量来记录),并把捕获寄存器中的值读取到 value1 中。当出现第二次上升沿时,发生 第 481 页 共 928 零死角玩转 STM32—F429 第二次捕获,计数器 CNT 的值会再次被锁存到捕获寄存器 CCR 中,并再次进入捕获中断, 在捕获中断中,把捕获寄存器的值读取到 value3 中,并清除捕获记录标志。利用 value3 和 value1 的差值我们就可以算出信号的周期(频率)。 2. 测量脉宽 当捕获通道 TIx 上出现上升沿时,发生第一次捕获,计数器 CNT 的值会被锁存到捕获 寄存器 CCR 中,而且还会进入捕获中断,在中断服务程序中记录一次捕获(可以用一个标 志变量来记录),并把捕获寄存器中的值读取到 value1 中。然后把捕获边沿改变为下降沿 捕获,目的是捕获后面的下降沿。当下降沿到来的时候,发生第二次捕获,计数器 CNT 的 值会再次被锁存到捕获寄存器 CCR 中,并再次进入捕获中断,在捕获中断中,把捕获寄存 器的值读取到 value3 中,并清除捕获记录标志。然后把捕获边沿设置为上升沿捕获。 在测量脉宽过程中需要来回的切换捕获边沿的极性,如果测量的脉宽时间比较长,定 时器就会发生溢出,溢出的时候会产生更新中断,我们可以在中断里面对溢出进行记录处 理。 32.3.2 PWM 输入模式 测量脉宽和频率还有一个更简便的方法就是使用 PWM 输入模式。与上面那种只使用 一个捕获寄存器测量脉宽和频率的方法相比,PWM 输入模式需要占用两个捕获寄存器。 图 32-12 输入通道和捕获通道的关系映射图 当使用 PWM 输入模式的时候,因为一个输入通道(TIx)会占用两个捕获通道(ICx),所 以一个定时器在使用 PWM 输入的时候最多只能使用两个输入通道(TIx)。 我们以输入通道 TI1 工作在 PWM 输入模式为例来讲解下具体的工作原理,其他通道 以此类推即可。 PWM 信号由输入通道 TI1 进入,因为是 PWM 输入模式的缘故,信号会被分为两路, 一路是 TI1FP1,另外一路是 TI2FP2。其中一路是周期,另一路是占空比,具体哪一路信 号对应周期还是占空比,得从程序上设置哪一路信号作为触发输入,作为触发输入的哪一 路信号对应的就是周期,另一路就是对应占空比。作为触发输入的那一路信号还需要设置 极性,是上升沿还是下降沿捕获,一旦设置好触发输入的极性,另外一路硬件就会自动配 第 482 页 共 928 零死角玩转 STM32—F429 置为相反的极性捕获,无需软件配置。一句话概括就是:选定输入通道,确定触发信号, 然后设置触发信号的极性即可,因为是 PWM 输入的缘故,另一路信号则由硬件配置,无 需软件配置。 当使用 PWM 输入模式的时候必须将从模式控制器配置为复位模式(配置寄存器 SMCR 的位 SMS[2:0]来实现),即当我们启动触发信号开始进行捕获的时候,同时把计数 器 CNT 复位清零。 下面我们以一个更加具体的时序图来分析下 PWM 输入模式。 图 32-13 PWM 输入模式时序 PWM 信号由输入通道 TI1 进入,配置 TI1FP1 为触发信号,上升沿捕获。当上升沿的 时候 IC1 和 IC2 同时捕获,计数器 CNT 清零,到了下降沿的时候,IC2 捕获,此时计数器 CNT 的值被锁存到捕获寄存器 CCR2 中,到了下一个上升沿的时候,IC1 捕获,计数器 CNT 的值被锁存到捕获寄存器 CCR1 中。其中 CCR2 测量的是脉宽,CCR1 测量的是周期。 从软件上来说,用 PWM 输入模式测量脉宽和周期更容易,付出的代价是需要占用两 个捕获寄存器。 32.4 输出比较应用 输出比较模式总共有 8 种,具体的由寄存器 CCMRx 的位 OCxM[2:0]配置。我们这里 只讲解最常用的 PWM 模式,其他几种模式具体的看数据手册即可。 32.4.1 PWM 输出模式 PWM 输出就是对外输出脉宽(即占空比)可调的方波信号,信号频率由自动重装寄 存器 ARR 的值决定,占空比由比较寄存器 CCR 的值决定。 PWM 模式分为两种,PWM1 和 PWM2,总得来说是差不多,就看你怎么用而已,具 体的区别见表格 32-1。 表格 32-1 PWM1 与 PWM2 模式的区别 模式 计数器 CNT 计算方式 PWM1 递增 递减 说明 CNTCCR,通道 CH 为无效,否则为有效 第 483 页 共 928 零死角玩转 STM32—F429 PWM2 递增 递减 CNTCCR,通道 CH 为有效,否则为无效 下面我们以 PWM1 模式来讲解,以计数器 CNT 计数的方向不同还分为边沿对齐模式 和中心对齐模式。PWM 信号主要都是用来控制电机,一般的电机控制用的都是边沿对齐 模式,FOC 电机一般用中心对齐模式。我们这里只分析这两种模式在信号感官上(即信号 波形)的区别,具体在电机控制中的区别不做讨论,到了你真正需要使用的时候就会知道 了。 1. PWM 边沿对齐模式 在递增计数模式下,计数器从 0 计数到自动重载值( TIMx_ARR 寄存器的内容),然后重新 从 0 开始计数并生成计数器上溢事件 图 32-14 PWM1 模式的边沿对齐波形 在边沿对齐模式下,计数器 CNT 只工作在一种模式,递增或者递减模式。这里我们以 CNT 工作在递增模式为例,在中,ARR=8,CCR=4,CNT 从 0 开始计数,当 CNTCCR 时,OCxREF 为无效的低电平,当 CCR=>CNT>=1 时,OCxREF 为有效的高电平。 在波形图上我们把波形分为两个阶段,第一个阶段是计数器 CNT 工作在递增模式的波 形,这个阶段我们又分为①和②两个阶段,第二个阶段是计数器 CNT 工作在递减模式的波 形,这个阶段我们又分为③和④两个阶段。要说中心对齐模式下的波形有什么特征的话, 那就是①和③阶段的时间相等,②和④阶段的时间相等。 中心对齐模式又分为中心对齐模式 1/2/3 三种,具体由寄存器 CR1 位 CMS[1:0]配置。 具体的区别就是比较中断中断标志位 CCxIF 在何时置 1:中心模式 1 在 CNT 递减计数的时 候置 1,中心对齐模式 2 在 CNT 递增计数时置 1,中心模式 3 在 CNT 递增和递减计数时都 置 1。 32.5 定时器初始化结构体详解 标准库函数对定时器外设建立了四个初始化结构体,分别为时基初始化结构体 TIM_TimeBaseInitTypeDef、输出比较初始化结构体 TIM_OCInitTypeDef、输入捕获初始化 结构体 TIM_ICInitTypeDef 和断路和死区初始化结构体 TIM_BDTRInitTypeDef,高级控制 定时器可以用到所有初始化结构体,通用定时器不能使用 TIM_BDTRInitTypeDef 结构体, 基本定时器只能使用时基结构体。初始化结构体成员用于设置定时器工作环境参数,并由 定时器相应初始化配置函数调用,最终这些参数将会写入到定时器相应的寄存器中。 初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个 成员意义基本上就可以对该外设运用自如。初始化结构体定义在 stm32f4xx_tim.h 文件中, 初始化库函数定义在 stm32f4xx_tim.c 文件中,编程时我们可以结合这两个文件内注释使用。 1. TIM_TimeBaseInitTypeDef 时基结构体 TIM_TimeBaseInitTypeDef 用于定时器基础参数设置,与 TIM_TimeBaseInit 函数配合使用完成配置。 代码清单 32-1 定时器基本初始化结构体 1 typedef struct { 2 uint16_t TIM_Prescaler; 3 uint16_t TIM_CounterMode; 4 uint32_t TIM_Period; 5 uint16_t TIM_ClockDivision; 6 uint8_t TIM_RepetitionCounter; 7 } TIM_TimeBaseInitTypeDef; // 预分频器 // 计数模式 // 定时器周期 // 时钟分频 // 重复计算器 (32) TIM_Prescaler:定时器预分频器设置,时钟源经该预分频器才是定时器计数时钟 CK_CNT,它设定 PSC 寄存器的值。计算公式为:计数器时钟频率 (fCK_CNT) 等于 fCK_PSC / (PSC[15:0] + 1),可实现 1 至 65536 分频。 (33) TIM_CounterMode:定时器计数方式,可设置为向上计数、向下计数以及中心对齐。 高级控制定时器允许选择任意一种。 第 485 页 共 928 零死角玩转 STM32—F429 (34) TIM_Period:定时器周期,实际就是设定自动重载寄存器 ARR 的值,ARR 为要装载 到实际自动重载寄存器(即影子寄存器)的值,可设置范围为 0 至 65535。 (35) TIM_ClockDivision:时钟分频,设置定时器时钟 CK_INT 频率与死区发生器以及数字 滤波器采样时钟频率分频比。可以选择 1、2、4 分频。 (36) TIM_RepetitionCounter:重复计数器,只有 8 位,只存在于高级定时器。 2. TIM_OCInitTypeDef 输出比较结构体 TIM_OCInitTypeDef 用于输出比较模式,与 TIM_OCxInit 函数配合使 用完成指定定时器输出通道初始化配置。高级控制定时器有四个定时器通道,使用时都必 须单独设置。 代码清单 32-2 定时器比较输出初始化结构体 1 typedef struct { 2 uint16_t TIM_OCMode; 3 uint16_t TIM_OutputState; 4 uint16_t TIM_OutputNState; 5 uint32_t TIM_Pulse; 6 uint16_t TIM_OCPolarity; 7 uint16_t TIM_OCNPolarity; 8 uint16_t TIM_OCIdleState; 9 uint16_t TIM_OCNIdleState; 10 } TIM_OCInitTypeDef; // 比较输出模式 // 比较输出使能 // 比较互补输出使能 // 脉冲宽度 // 输出极性 // 互补输出极性 // 空闲状态下比较输出状态 // 空闲状态下比较互补输出状态 (1) TIM_OCMode:比较输出模式选择,总共有八种,常用的为 PWM1/PWM2。它设定 CCMRx 寄存器 OCxM[2:0]位的值。 (2) TIM_OutputState:比较输出使能,决定最终的输出比较信号 OCx 是否通过外部引脚输 出。它设定 TIMx_CCER 寄存器 CCxE/CCxNE 位的值。 (3) TIM_OutputNState:比较互补输出使能,决定 OCx 的互补信号 OCxN 是否通过外部引脚 输出。它设定 CCER 寄存器 CCxNE 位的值。 (4) TIM_Pulse:比较输出脉冲宽度,实际设定比较寄存器 CCR 的值,决定脉冲宽度。可 设置范围为 0 至 65535。 (5) TIM_OCPolarity:比较输出极性,可选 OCx 为高电平有效或低电平有效。它决定着定 时器通道有效电平。它设定 CCER 寄存器的 CCxP 位的值。 (6) TIM_OCNPolarity:比较互补输出极性,可选 OCxN 为高电平有效或低电平有效。它 设定 TIMx_CCER 寄存器的 CCxNP 位的值。 (7) TIM_OCIdleState:空闲状态时通道输出电平设置,可选输出 1 或输出 0,即在空闲状 态(BDTR_MOE 位为 0)时,经过死区时间后定时器通道输出高电平或低电平。它设定 CR2 寄存器的 OISx 位的值。 (8) TIM_OCNIdleState:空闲状态时互补通道输出电平设置,可选输出 1 或输出 0,即在 空闲状态(BDTR_MOE 位为 0)时,经过死区时间后定时器互补通道输出高电平或低电 平,设定值必须与 TIM_OCIdleState 相反。它设定是 CR2 寄存器的 OISxN 位的值。 3. TIM_ICInitTypeDef 输入捕获结构体 TIM_ICInitTypeDef 用于输入捕获模式,与 TIM_ICInit 函数配合使用 完成定时器输入通道初始化配置。如果使用 PWM 输入模式需要与 TIM_PWMIConfig 函数 配合使用完成定时器输入通道初始化配置。 第 486 页 共 928 零死角玩转 STM32—F429 代码清单 32-3 定时器输入捕获初始化结构体 1 typedef struct { 2 uint16_t TIM_Channel; 3 uint16_t TIM_ICPolarity; 4 uint16_t TIM_ICSelection; 5 uint16_t TIM_ICPrescaler; 6 uint16_t TIM_ICFilter; 7 } TIM_ICInitTypeDef; // 输入通道选择 // 输入捕获触发选择 // 输入捕获选择 // 输入捕获预分频器 // 输入捕获滤波器 (1) TIM_Channel:捕获通道 ICx 选择,可选 TIM_Channel_1、TIM_Channel_2、 TIM_Channel_3 或 TIM_Channel_4 四个通道。它设定 CCMRx 寄存器 CCxS 位 的值。 (2) TIM_ICPolarity:输入捕获边沿触发选择,可选上升沿触发、下降沿触发或边沿跳变触 发。它设定 CCER 寄存器 CCxP 位和 CCxNP 位的值。 (3) TIM_ICSelection:输入通道选择,捕获通道 ICx 的信号可来自三个输入通道,分别为 TIM_ICSelection_DirectTI、TIM_ICSelection_IndirectTI 或 TIM_ICSelection_TRC,具 体的区别见图 32-16。它设定 CCRMx 寄存器的 CCxS[1:0]位的值。 图 32-16 输入通道与捕获通道 IC 的映射图 (4) TIM_ICPrescaler:输入捕获通道预分频器,可设置 1、2、4、8 分频,它设定 CCMRx 寄存器的 ICxPSC[1:0]位的值。如果需要捕获输入信号的每个有效边沿,则设置 1 分频 即可。 (5) TIM_ICFilter:输入捕获滤波器设置,可选设置 0x0 至 0x0F。它设定 CCMRx 寄存器 ICxF[3:0]位的值。一般我们不使用滤波器,即设置为 0。 4. TIM_BDTRInitTypeDef 断路和死区结构体 TIM_BDTRInitTypeDef 用于断路和死区参数的设置,属于高级定时 器专用,用于配置断路时通道输出状态,以及死区时间。它与 TIM_BDTRConfig 函数配置 使用完成参数配置。这个结构体的成员只对应 BDTR 这个寄存器,有关成员的具体使用配 置请参考手册 BDTR 寄存器的详细描述。 代码清单 32-4 断路和死区初始化结构体 1 typedef struct { 2 uint16_t TIM_OSSRState; 3 uint16_t TIM_OSSIState; 4 uint16_t TIM_LOCKLevel; 5 uint16_t TIM_DeadTime; 6 uint16_t TIM_Break; // 运行模式下的关闭状态选择 // 空闲模式下的关闭状态选择 // 锁定配置 // 死区时间 // 断路输入使能控制 第 487 页 共 928 零死角玩转 STM32—F429 7 uint16_t TIM_BreakPolarity; // 断路输入极性 8 uint16_t TIM_AutomaticOutput; // 自动输出使能 9 } TIM_BDTRInitTypeDef; (1) TIM_OSSRState:运行模式下的关闭状态选择,它设定 BDTR 寄存器 OSSR 位的值。 (2) TIM_OSSIState:空闲模式下的关闭状态选择,它设定 BDTR 寄存器 OSSI 位的值。 (3) TIM_LOCKLevel:锁定级别配置, BDTR 寄存器 LOCK[1:0]位的值。 (4) TIM_DeadTime:配置死区发生器,定义死区持续时间,可选设置范围为 0x0 至 0xFF。 它设定 BDTR 寄存器 DTG[7:0]位的值。 (5) TIM_Break:断路输入功能选择,可选使能或禁止。它设定 BDTR 寄存器 BKE 位的值。 (6) TIM_BreakPolarity:断路输入通道 BRK 极性选择,可选高电平有效或低电平有效。它 设定 BDTR 寄存器 BKP 位的值。 (7) TIM_AutomaticOutput:自动输出使能,可选使能或禁止,它设定 BDTR 寄存器 AOE 位的值。 32.6 PWM 互补输出实验 输出比较模式比较多,这里我们以 PWM 输出为例讲解,并通过示波器来观察波形。 实验中不仅在主输出通道输出波形,还在互补通道输出与主通道互补的的波形,并且添加 了断路和死区功能。 32.6.1 硬件设计 根据开发板引脚使用情况,并且参考表 32-1 中定时器引脚信息 ,使用 TIM8 的通道 1 及其互补通道作为本实验的波形输出通道,对应选择 PC6 和 PA5 引脚。将示波器的两个输 入通道分别与 PC6 和 PA5 引脚短接,用于观察波形,还有注意共地。 为增加断路功能,需要用到 TIM8_BKIN 引脚,这里选择 PA6 引脚。程序我们设置该 引脚为低电平有效,所以先使用杜邦线将该引脚与开发板上 3.3V 短接。 另外,实验用到两个按键用于调节 PWM 的占空比大小,直接使用开发板上独立按键 即可,电路参考独立按键相关章节。 32.6.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_advance_tim.c 和 bsp_advance_tim.h 文件用来存定时器驱动程序及相关宏定义。 1. 编程要点 (1) 定时器 IO 配置 (2) 定时器时基结构体 TIM_TimeBaseInitTypeDef 配置 (3) 定时器输出比较结构体 TIM_OCInitTypeDef 配置 (4) 定时器断路和死区结构体 TIM_BDTRInitTypeDef 配置 第 488 页 共 928 零死角玩转 STM32—F429 2. 软件分析 宏定义 代码清单 32-5 宏定义 1 /* 定时器 */ 2 #define ADVANCE_TIM 3 #define ADVANCE_TIM_CLK 4 5 /* TIM8 通道 1 输出引脚 */ 6 #define ADVANCE_OCPWM_PIN 7 #define ADVANCE_OCPWM_GPIO_PORT 8 #define ADVANCE_OCPWM_GPIO_CLK 9 #define ADVANCE_OCPWM_PINSOURCE 10 #define ADVANCE_OCPWM_AF 11 12 /* TIM8 通道 1 互补输出引脚 */ 13 #define ADVANCE_OCNPWM_PIN 14 #define ADVANCE_OCNPWM_GPIO_PORT 15 #define ADVANCE_OCNPWM_GPIO_CLK 16 #define ADVANCE_OCNPWM_PINSOURCE 17 #define ADVANCE_OCNPWM_AF 18 19 /* TIM8 断路输入引脚 */ 20 #define ADVANCE_BKIN_PIN 21 #define ADVANCE_BKIN_GPIO_PORT 22 #define ADVANCE_BKIN_GPIO_CLK 23 #define ADVANCE_BKIN_PINSOURCE 24 #define ADVANCE_BKIN_AF TIM8 RCC_APB2Periph_TIM8 GPIO_Pin_6 GPIOC RCC_AHB1Periph_GPIOC GPIO_PinSource6 GPIO_AF_TIM8 GPIO_Pin_5 GPIOA RCC_AHB1Periph_GPIOA GPIO_PinSource5 GPIO_AF_TIM8 GPIO_Pin_6 GPIOA RCC_AHB1Periph_GPIOA GPIO_PinSource6 GPIO_AF_TIM8 使用宏定义非常方便程序升级、移植。如果使用不同的定时器 IO,修改这些宏即可。 定时器复用功能引脚初始化 代码清单 32-6 定时器复用功能引脚初始化 1 static void TIMx_GPIO_Config(void) 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 /*定义一个 GPIO_InitTypeDef 类型的结构体*/ GPIO_InitTypeDef GPIO_InitStructure; /*开启定时器相关的 GPIO 外设时钟*/ RCC_AHB1PeriphClockCmd (ADVANCE_OCPWM_GPIO_CLK | ADVANCE_OCNPWM_GPIO_CLK | ADVANCE_BKIN_GPIO_CLK, ENABLE); /* 指定引脚复用功能 */ GPIO_PinAFConfig(ADVANCE_OCPWM_GPIO_PORT, ADVANCE_OCPWM_PINSOURCE, ADVANCE_OCPWM_AF); GPIO_PinAFConfig(ADVANCE_OCNPWM_GPIO_PORT, ADVANCE_OCNPWM_PINSOURCE, ADVANCE_OCNPWM_AF); GPIO_PinAFConfig(ADVANCE_BKIN_GPIO_PORT, ADVANCE_BKIN_PINSOURCE, ADVANCE_BKIN_AF); /* 定时器功能引脚初始化 */ GPIO_InitStructure.GPIO_Pin = ADVANCE_OCPWM_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; GPIO_Init(ADVANCE_OCPWM_GPIO_PORT, &GPIO_InitStructure); 第 489 页 共 928 零死角玩转 STM32—F429 29 30 31 32 33 34 35 } GPIO_InitStructure.GPIO_Pin = ADVANCE_OCNPWM_PIN; GPIO_Init(ADVANCE_OCNPWM_GPIO_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = ADVANCE_BKIN_PIN; GPIO_Init(ADVANCE_BKIN_GPIO_PORT, &GPIO_InitStructure); 定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时 器。使用 GPIO 之前都必须开启相应端口时钟。 定时器模式配置 代码清单 32-7 定时器模式配置 1 static void TIM_Mode_Config(void) 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 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; //开启 TIMx_CLK,x[1,8]时钟 RCC_APB2PeriphClockCmd(ADVANCE_TIM_CLK, ENABLE); //累计 TIM_Period 个后产生一个更新或者中断 //当定时器从 0 计数到 1023,即为 1024 次,为一个定时周期 TIM_TimeBaseStructure.TIM_Period = 1024-1; //高级控制定时器时钟源 TIMxCLK = HCLK=180MHz //设定定时器计数器频率为=TIMxCLK/(TIM_Prescaler+1)=100KHz TIM_TimeBaseStructure.TIM_Prescaler = 1800-1; //采样时钟分频,这里不需要用到 TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //计数方式 TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //重复计数器,这里不需要用到 TIM_TimeBaseStructure.TIM_RepetitionCounter=0; //初始化定时器 TIMx, x[1,8] TIM_TimeBaseInit(ADVANCE_TIM, &TIM_TimeBaseStructure); //PWM 模式配置 //配置为 PWM 模式 1 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse = ChannelPulse; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState = TIM_OCIdleState_Set; TIM_OCInitStructure.TIM_OCNIdleState = TIM_OCNIdleState_Reset; //初始化输出比较通道 TIM_OC1Init(ADVANCE_TIM, &TIM_OCInitStructure); //使能 ARR 寄存器预装载 TIM_OC1PreloadConfig(ADVANCE_TIM, TIM_OCPreload_Enable); //自动输出使能,断路、死区时间和锁定配置 TIM_BDTRInitStructure.TIM_OSSRState = TIM_OSSRState_Enable; TIM_BDTRInitStructure.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStructure.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStructure.TIM_DeadTime = 11; TIM_BDTRInitStructure.TIM_Break = TIM_Break_Enable; TIM_BDTRInitStructure.TIM_BreakPolarity = TIM_BreakPolarity_Low; TIM_BDTRInitStructure.TIM_AutomaticOutput=TIM_AutomaticOutput_Enable; TIM_BDTRConfig(ADVANCE_TIM, &TIM_BDTRInitStructure); 第 490 页 共 928 零死角玩转 STM32—F429 51 52 53 54 55 56 } //使能定时器,计数器开始计数 TIM_Cmd(ADVANCE_TIM, ENABLE); //主动输出使能 TIM_CtrlPWMOutputs(ADVANCE_TIM, ENABLE); 首先定义三个定时器初始化结构体,定时器模式配置函数主要就是对这三个结构体的 成员进行初始化,然后通过相应的初始化函数把这些参数写人定时器的寄存器中。有关结 构体的成员介绍请参考定时器初始化结构体详解小节。 不同的定时器可能对应不同的 APB 总线,在使能定时器时钟是必须特别注意。高级控 制定时器属于 APB2,定时器内部时钟是 180MHz。 在时基结构体中我们设置定时器周期参数为 1024,频率为 100KHz,使用向上计数方 式。因为我们使用的是内部时钟,所以外部时钟采样分频成员不需要设置,重复计数器我 们没用到,也不需要设置。 在输出比较结构体中,设置输出模式为 PWM1 模式,主通道和互补通道输出均使能, 且高电平有效,设置脉宽为 ChannelPulse,ChannelPulse 是我们定义的一个无符号 16 位整 形的全局变量,用来指定占空比大小,实际上脉宽就是设定比较寄存器 CCR 的值,用于跟 计数器 CNT 的值比较。 断路和死区结构体中,使能断路功能,设定断路信号的有效极性,设定死区时间。 最后使能定时器让计数器开始计数和通道主输出。 主函数 代码清单 32-8 main 函数 1 int main(void) 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 } /* 初始化按键 GPIO */ Key_GPIO_Config(); /* 初始化高级控制定时器,设置 PWM 模式,使能通道 1 互补输出 */ TIMx_Configuration(); while (1) { /* 扫描 KEY1 */ if ( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON ) { /* 增大占空比 */ if (ChannelPulse<960) ChannelPulse+=64; else ChannelPulse=1024; TIM_SetCompare1(ADVANCE_TIM,ChannelPulse); } /* 扫描 KEY2 */ if ( Key_Scan(KEY2_GPIO_PORT,KEY2_PIN) == KEY_ON ) { /* 减小占空比 */ if (ChannelPulse>=64) ChannelPulse-=64; else ChannelPulse=0; TIM_SetCompare1(ADVANCE_TIM,ChannelPulse); } } 第 491 页 共 928 零死角玩转 STM32—F429 首先,调用 Key_GPIO_Config 函数完成按键引脚初始化配置,该函数定义在 bsp_key.c 文件中。 接下来,调用 TIMx_Configuration 函数完成定时器参数配置,包括定时器复用引脚配 置和定时器模式配置,该函数定义在 bsp_advance_tim.c 文件中它实际上只是简单的调用 TIMx_GPIO_Config 函数和 TIM_Mode_Config 函数。运行完该函数后通道引脚就已经有 PWM 波形输出,通过示波器可直观观察到。 最后,在无限循环函数中检测按键状态,如果是 KEY1 被按下,就增加 ChannelPulse 变量值,并调用 TIM_SetCompare1 函数完成增加占空比设置;如果是 KEY2 被按下,就减 小 ChannelPulse 变量值,并调用 TIM_SetCompare1 函数完成减少占空比设置。 TIM_SetCompare1 函数实际是设定 TIMx_CCR1 寄存器值。 32.6.3 下载验证 根据实验的硬件设计内容接好示波器输入通道和开发板引脚连接,并把断路输入引脚 拉高。编译实验程序并下载到开发板上,调整示波器到合适参数,在示波器显示屏和看到 一路互补的 PWM 波形,参考图 32-17。此时,按下开发板上 KEY1 或 KEY2 可改变波形的 占空比。 图 32-17 PWM 互补波形输出示波器图 第 492 页 共 928 零死角玩转 STM32—F429 32.7 PWM 输入捕获实验 实验中,我们用通用定时器产生已知频率和占空比的 PWM 信号,然后用高级定时器 的 PWM 输入模式来测量这个已知的 PWM 信号的频率和占空比,通过两者的对比即可知 道测量是否准确。 32.7.1 硬件设计 实验中用到两个引脚,一个是通用定时器通道用于波形输出,另一个是高级控制定时 器通道用于输入捕获,实验中直接使用一根杜邦线短接即可。 32.7.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_advance_tim.c 和 bsp_advance_tim.h 文件用来存定时器驱动程序及相关宏定义。 1. 编程要点 (1) 通用定时器产生 PWM 配置 (2) 高级定时器 PWM 输入配置 (3) 计算测量的频率和占空比,并打印出来比较 2. 软件分析 宏定义 代码清单 32-9 宏定义 1 /* 通用定时器 PWM 输出 */ 2 /* PWM 输出引脚 */ 3 #define GENERAL_OCPWM_PIN 4 #define GENERAL_OCPWM_GPIO_PORT 5 #define GENERAL_OCPWM_GPIO_CLK 6 #define GENERAL_OCPWM_PINSOURCE 7 #define GENERAL_OCPWM_AF 8 9 /* 通用定时器 */ 10 #define GENERAL_TIM 11 #define GENERAL_TIM_CLK 12 13 /* 高级控制定时器 PWM 输入捕获 */ 14 /* PWM 输入捕获引脚 */ 15 #define ADVANCE_ICPWM_PIN 16 #define ADVANCE_ICPWM_GPIO_PORT 17 #define ADVANCE_ICPWM_GPIO_CLK 18 #define ADVANCE_ICPWM_PINSOURCE 19 #define ADVANCE_ICPWM_AF 20 #define ADVANCE_IC1PWM_CHANNEL 21 #define ADVANCE_IC2PWM_CHANNEL 22 23 /* 高级控制定时器 */ 24 #define ADVANCE_TIM 25 #define ADVANCE_TIM_CLK GPIO_Pin_5 GPIOA RCC_AHB1Periph_GPIOA GPIO_PinSource5 GPIO_AF_TIM2 TIM2 RCC_APB1Periph_TIM2 GPIO_Pin_6 GPIOC RCC_AHB1Periph_GPIOC GPIO_PinSource6 GPIO_AF_TIM8 TIM_Channel_1 TIM_Channel_2 TIM8 RCC_APB2Periph_TIM8 第 493 页 共 928 零死角玩转 STM32—F429 26 27 /* 捕获/比较中断 */ 28 #define ADVANCE_TIM_IRQn 29 #define ADVANCE_TIM_IRQHandler TIM8_CC_IRQn TIM8_CC_IRQHandler 使用宏定义非常方便程序升级、移植。如果使用不同的定时器 IO,修改这些宏即可。 定时器复用功能引脚初始化 代码清单 32-10 定时器复用功能引脚初始化 1 static void TIMx_GPIO_Config(void) 2{ 3 /*定义一个 GPIO_InitTypeDef 类型的结构体*/ 4 GPIO_InitTypeDef GPIO_InitStructure; 5 6 /*开启 LED 相关的 GPIO 外设时钟*/ 7 RCC_AHB1PeriphClockCmd (GENERAL_OCPWM_GPIO_CLK, ENABLE); 8 RCC_AHB1PeriphClockCmd (ADVANCE_ICPWM_GPIO_CLK, ENABLE); 9 10 /* 定时器复用引脚 */ 11 GPIO_PinAFConfig(GENERAL_OCPWM_GPIO_PORT, 12 GENERAL_OCPWM_PINSOURCE, 13 GENERAL_OCPWM_AF); 14 GPIO_PinAFConfig(ADVANCE_ICPWM_GPIO_PORT, 15 ADVANCE_ICPWM_PINSOURCE, 16 ADVANCE_ICPWM_AF); 17 18 /* 通用定时器 PWM 输出引脚 */ 19 GPIO_InitStructure.GPIO_Pin = GENERAL_OCPWM_PIN; 20 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 21 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 22 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 23 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 24 GPIO_Init(GENERAL_OCPWM_GPIO_PORT, &GPIO_InitStructure); 25 26 /* 高级控制定时器 PWM 输入捕获引脚 */ 27 GPIO_InitStructure.GPIO_Pin = ADVANCE_ICPWM_PIN; 28 GPIO_Init(ADVANCE_ICPWM_GPIO_PORT, &GPIO_InitStructure); 29 } 定时器通道引脚使用之前必须设定相关参数,这选择复用功能,并指定到对应的定时 器。使用 GPIO 之前都必须开启相应端口时钟。 嵌套向量中断控制器组配置 代码清单 32-11 NVIC 配置 1 static void TIMx_NVIC_Configuration(void) 2{ 3 NVIC_InitTypeDef NVIC_InitStructure; 4 // 设置中断组为 0 5 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); 6 // 设置中断来源 7 NVIC_InitStructure.NVIC_IRQChannel = ADVANCE_TIM_IRQn; 8 // 设置抢占优先级 9 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; 10 // 设置子优先级 11 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; 12 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 13 NVIC_Init(&NVIC_InitStructure); 14 } 实验用到高级控制定时器捕获/比较中断,需要配置中断优先级,因为实验只用到一个 中断,所以这里对优先级配置没具体要求,只要符合中断组参数要求即可。 第 494 页 共 928 零死角玩转 STM32—F429 通用定时器 PWM 输出 代码清单 32-12 通用定时器 PWM 输出 1 static void TIM_PWMOUTPUT_Config(void) 2{ 3 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 4 TIM_OCInitTypeDef TIM_OCInitStructure; 5 6 // 开启 TIMx_CLK,x[2,3,4,5,12,13,14] 7 RCC_APB1PeriphClockCmd(GENERAL_TIM_CLK, ENABLE); 8 9 //累计 TIM_Period 个后产生一个更新或者中断 10 //当定时器从 0 计数到 8999,即为 9000 次,为一个定时周期 11 TIM_TimeBaseStructure.TIM_Period = 10000-1; 12 13 // 通用定时器 2 时钟源 TIMxCLK = HCLK/2=90MHz 14 // 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=100KHz 15 TIM_TimeBaseStructure.TIM_Prescaler = 900-1; 16 // 采样时钟分频,这里不需要用到 17 TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; 18 // 计数方式 19 TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; 20 21 // 初始化定时器 TIMx, x[1,8] 22 TIM_TimeBaseInit(GENERAL_TIM, &TIM_TimeBaseStructure); 23 24 //PWM 输出模式配置 25 //配置为 PWM 模式 1 26 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; 27 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; 28 //PWM 脉冲宽度 29 TIM_OCInitStructure.TIM_Pulse = 3000-1; 30 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; 31 // 输出比较通道初始化 32 TIM_OC1Init(GENERAL_TIM, &TIM_OCInitStructure); 33 // 使能 ARR 预装载 34 TIM_OC1PreloadConfig(GENERAL_TIM, TIM_OCPreload_Enable); 35 // 使能定时器,计数器开始计数 36 TIM_Cmd(GENERAL_TIM, ENABLE); 37 } 定时器 PWM 输出模式配置函数很简单,看代码注释即可。这里我们设置了 PWM 的 频率为 100KHZ,即周期为 10ms,占空比为:(Pulse+1)/(Period+1) = 30%。 高级控制定时 PWM 输入模式 代码清单 32-13 PWM 输入模式配置 1 static void TIM_PWMINPUT_Config(void) 2{ 3 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 4 TIM_ICInitTypeDef TIM_ICInitStructure; 5 6 // 开启 TIMx_CLK,x[1,8] 7 RCC_APB2PeriphClockCmd(ADVANCE_TIM_CLK, ENABLE); 8 9 TIM_TimeBaseStructure.TIM_Period = 0xFFFF-1; 10 // 高级控制定时器时钟源 TIMxCLK = HCLK=180MHz 11 // 设定定时器频率为=TIMxCLK/(TIM_Prescaler+1)=100KHz 12 TIM_TimeBaseStructure.TIM_Prescaler = 1800-1; 13 // 计数方式 14 TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; 15 // 初始化定时器 TIMx, x[1,8] 16 TIM_TimeBaseInit(ADVANCE_TIM, &TIM_TimeBaseStructure); 第 495 页 共 928 零死角玩转 STM32—F429 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 } // 捕获通道 IC1 配置 // 选择捕获通道 TIM_ICInitStructure.TIM_Channel = ADVANCE_IC1PWM_CHANNEL; // 设置捕获的边沿 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 设置捕获通道的信号来自于哪个输入通道 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 1 分频,即捕获信号的每个有效边沿都捕获 TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 不滤波 TIM_ICInitStructure.TIM_ICFilter = 0x0; // 初始化 PWM 输入模式 TIM_PWMIConfig(ADVANCE_TIM, &TIM_ICInitStructure); // 当工作做 PWM 输入模式时,只需要设置触发信号的那一路即可(用于测量周期) // 另外一路(用于测量占空比)会由硬件自带设置 // 捕获通道 IC2 配置 TIM_ICInitStructure.TIM_Channel = ADVANCE_IC2PWM_CHANNEL; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0; TIM_PWMIConfig(ADVANCE_TIM, &TIM_ICInitStructure); //选择输入捕获的触发信号 TIM_SelectInputTrigger(ADVANCE_TIM, TIM_TS_TI1FP1); //选择从模式: 复位模式 //PWM 输入模式时,从模式必须工作在复位模式,当捕获开始时,计数器 CNT 会被复位 TIM_SelectSlaveMode(ADVANCE_TIM, TIM_SlaveMode_Reset); TIM_SelectMasterSlaveMode(ADVANCE_TIM,TIM_MasterSlaveMode_Enable); //使能高级控制定时器,计数器开始计数 TIM_Cmd(ADVANCE_TIM, ENABLE); //使能捕获中断,这个中断针对的是主捕获通道(测量周期那个) TIM_ITConfig(ADVANCE_TIM, TIM_IT_CC1, ENABLE); 输入捕获配置中,主要初始化两个结构体,时基结构体部分很简单,看注释理解即可。 关键部分是输入捕获结构体的初始化。 首先,我们要选定捕获通道,这里我们用 IC1,然后设置捕获信号的极性,这里我们 配置为上升沿,我们需要对捕获信号的每个有效边沿(即我们设置的上升沿)都捕获,所 以我们不分频,滤波器我们也不需要用。那么捕获通道的信号来源于哪里呢?IC1 的信号 可以是 TI1 输入的 TI1FP1,也可以是从 TI2 输入的 TI2FP1,我们这里选择直连 (DirectTI),即 IC1 映射到 TI1FP1,即 PWM 信号从 TI1 输入。 我们知道,PWM 输入模式,需要使用两个捕获通道,占用两个捕获寄存器。由输入 通道 TI1 输入的信号会分成 TI1FP1 和 TI1FP2,具体选择哪一路信号作为捕获触发信号决 定着哪个捕获通道测量的是周期。这里我们选择 TI1FP1 作为捕获的触发信号,那 PWM 信 号的周期则存储在 CCR1 寄存器中,剩下的另外一路信号 TI1FP2 则进入 IC2,CCR2 寄存 器存储的是脉冲宽度。 PWM 输入模式虽然占用了两个通道,但是我们只需要配置触发信号那一路即可,剩 下的另外一个通道会由硬件自动配置,软件无需配置。 。 第 496 页 共 928 零死角玩转 STM32—F429 高级控制定时器中断服务函数 代码清单 32-14 高级控制定时器中断服务函数 1 void ADVANCE_TIM_IRQHandler (void) 2{ 3 /* 清除定时器捕获/比较 1 中断 */ 4 TIM_ClearITPendingBit(ADVANCE_TIM, TIM_IT_CC1); 5 6 /* 获取输入捕获值 */ 7 IC1Value = TIM_GetCapture1(ADVANCE_TIM); 8 IC2Value = TIM_GetCapture2(ADVANCE_TIM); 9 printf("IC1Value = %d IC2Value = %d ",IC1Value,IC2Value); 10 11 if (IC1Value != 0) { 12 /* 占空比计算 */ 13 DutyCycle = (float)(IC2Value * 100) / IC1Value; 14 15 /* 频率计算 */ 16 Frequency = 180000000/1800/(float)IC1Value; 17 printf("占空比:%0.2f%% 频率:%0.2fHz\n",DutyCycle,Frequency); 18 } else { 19 DutyCycle = 0; 20 Frequency = 0; 21 } 22 } 中断复位函数中,我们获取 CCR1 和 CCR2 寄存器中的值,当 CCR1 的值不为 0 时, 说明有效捕获到了一个周期,然后计算出频率和占空比。 如果是第一个上升沿中断,计数器会被复位,锁存到 CCR1 寄存器的值是 0,CCR2 寄 存器的值也是 0,无法计算频率和占空比。当第二次上升沿到来的时候,CCR1 和 CCR2 捕 获到的才是有效的值。 主函数 代码清单 32-15 main 函数 1 int main(void) 2{ 3 Debug_USART_Config(); 4 5 /* 初始化高级控制定时器输入捕获以及通用定时器输出 PWM */ 6 TIMx_Configuration(); 7 8 while (1) { 9 } 10 } 主函数内容非常简单,首先调用 Debug_USART_Config 函数完成串口初始化配置,该 函定义在 bsp_debug_usart.c 文件内。 接下来就是调用 TIMx_Configuration 函数完成定时器配置,该函数定义在 bsp_advance_tim.c 文件内,它只是简单的分别调用 TIMx_GPIO_Config()、 TIMx_NVIC_Configuration()、TIM_PWMOUTPUT_Config()和 TIM_PWMINPUT_Config()四 个函数,完成定时器引脚初始化配置,NVIC 配置,通用定时器输出 PWM 以及高级控制定 时器 PWM 输入模式配置。 主函数的无限循环不需要执行任何程序,任务执行在定时器中断服务函数完成。 第 497 页 共 928 零死角玩转 STM32—F429 32.7.3 下载验证 根据硬件设计内容结合软件设计的引脚宏定义参数,用杜邦线连接通用定时器 PWM 输出引脚和高级控制定时器的输入捕获引脚。使用 USB 线连接开发板上的“USB TO UART”接口到电脑,电脑端配置好串口调试助手参数。编译实验程序并下载到开发板上, 程序运行后在串口调试助手可接收到开发板发过来有关测量波形的参数信息。 32.8 每课一问 13. 计算高级控制定时器一次最长定时时间。 14. PWM 输入捕获实验中,根据程序参数,求通用定时器输出的 PWM 波形频率。 第 498 页 共 928 零死角玩转 STM32—F429 第33章 TIM—电容按键检测 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》。 前面章节我们讲解了基本定时器和高级控制定时器功能,这一章我们将介绍定时器输 入捕获一个应用实例,帮助我们更加深入理解定时器。 33.1 电容按键原理 电容器(简称为电容)就是可以容纳电荷的器件,两个金属块中间隔一层绝缘体就可以 构成一个最简单的电容。如图 33-1(俯视图),有两个金属片,之间有一个绝缘介质,这样 就构成了一个电容。这样一个电容在电路板上非常容易实现,一般设计四周的铜片与电路 板地信号连通,这样一种结构就是电容按键的模型。当电路板形状固定之后,该电容的容 量也是相对稳定的。 图 33-1 片状电容器 电路板制作时都会在表面上覆盖一层绝缘层,用于防腐蚀和绝缘,所以实际电路板设 计时情况如图 33-2。电路板最上层是绝缘材料,下面一层是导电铜箔,我们根据电路走线 情况设计决定铜箔的形状,再下面一层一般是 FR-4 板材。金属感应片与地信号之间有绝缘 材料隔着,整个可以等效为一个电容 Cx。一般在设计时候,把金属感应片设计成方便手指 触摸大小。 第 499 页 共 928 零死角玩转 STM32—F429 图 33-2 无手指触摸情况 在电路板未上电时,可以认为电容 Cx 是没有电荷的,在上电时,在电阻作用下,电 容 Cx 就会有一个充电过程,直到电容充满,即 Vc 电压值为 3.3V,这个充电过程的时间长 短受到电阻 R 阻值和电容 Cx 容值的直接影响。但是在我们选择合适电阻 R 并焊接固定到 电路板上后,这个充电时间就基本上不会变了,因为此时电阻 R 已经是固定的,电容 Cx 在无外界明显干扰情况下基本上也是保持不变的。 现在,我们来看看当我们用手指触摸时会是怎样一个情况?如图 33-3,当我们用手指 触摸时,金属感应片除了与地信号形成一个等效电容 Cx 外,还会与手指形成一个 Cs 等效 电容。 图 33-3 有手指触摸情况 此时整个电容按键可以容纳的电荷数量就比没有手指触摸时要多了,可以看成是 Cx 和 Cs 叠加的效果。在相同的电阻 R 情况下,因为电容容值增大了,导致需要更长的充电 时间。也就是这个充电时间变长使得我们区分有无手指触摸,也就是电容按键是否被按下。 第 500 页 共 928 零死角玩转 STM32—F429 现在最主要的任务就是测量充电时间。充电过程可以看出是一个信号从低电平变成高 电平的过程,现在就是要求出这个变化过程的时间,这样的一个命题与上一章讲解高级控 制定时器的输入捕获功能非常吻合。我们可以利用定时器输入捕获功能计算充电时间,即 设置 TIMx_CH 为定时器输入捕获模式通道。这样先测量得到无触摸时的充电时间作为比 较基准,然后再定时循环测量充电时间与无触摸时的充电时间作比较,如果超过一定的阈 值就认为是有手指触摸。 图 33-4 为 Vc 跟随时间变化情况,可以看出在无触摸情况下,电压变化较快;而在有 触摸时,总的电容量增大了,电压变化缓慢一些。 图 33-4 Vc 电压与充电时间关系 为测量充电时间,我们需要设置定时器输入捕获功能为上升沿触发,图 33-4 中 VH 就 是被触发上升沿的电压值,也是 STM32 认为是高电平的最低电压值,大约为 1.8V。t1 和 t2 可以通过定时器捕获/比较寄存器获取得到。 不过,在测量充电时间之前,我们必须想办法制作这个充电过程。之前的分析是在电 路板上电时会有充电过程,现在我们要求在程序运行中循环检测按键,所以必须可以控制 充电过程的生成。我们可以控制 TIMx_CH 引脚作为普通的 GPIO 使用,使其输出一小段时 间的低电平,为电容 Cx 放电,即 Vc 为 0V。当我们重新配置 TIMx_CH 为输入捕获时电容 Cx 在电阻 R 的作用下就可以产生充电过程。 33.2 电容按键检测实验 电容按键不需要任何外部机械部件,使用方便,成本低,很容易制成与周围环境相密 封的键盘,以起到防潮防湿的作用。电容按键优势突出使得越来越多电子产品使用它代替 传统的机械按键。 本实验实现电容按键状态检测方法,提供一个编程实例。 33.2.1 硬件设计 开发板板载一个电容按键,原理图设计参考图 33-5。 第 501 页 共 928 零死角玩转 STM32—F429 图 33-5 电容按键电路设计 标示 TPAD1 在电路板上就是电容按键实体,它通过一根导线连接至定时器通道引脚, 这里选用的电阻阻值为 1M。 实验还用到调试串口和蜂鸣器功能,用来打印输入捕获信息和指示按键状态,这两个 模块电路可参考之前相关章节。 33.2.2 软件设计 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到,完整 的代码请参考本章配套的工程。我们创建了两个文件:bsp_touchpad.c 和 bsp_touchpad.h 文 件用来存放电容按键检测相关函数和宏定义。 1. 编程要点 (5) 初始化蜂鸣器、调试串口以及系统滴答定时器; (6) 配置定时器基本初始化结构体并完成定时器基本初始化; (7) 配置定时器输入捕获功能; (8) 使能电容按键引脚输出低电平为电容按键放电; (9) 待放电完整后,配置为输入捕获模式,并获取输入捕获值,该值即为无触摸时输入捕 获值; (10) 循环执行电容按键放电、读取输入捕获值检过程,将捕获值与无触摸时捕获值对比, 以确定电容按键状态。 2. 软件分析 宏定义 代码清单 33-1 宏定义 1 #define TPAD_TIMx 2 #define TPAD_TIM_CLK 3 4 #define TPAD_TIM_Channel_X 5 #define TPAD_TIM_IT_CCx 6 #define TPAD_TIM_GetCaptureX 7 TIM2 RCC_APB1Periph_TIM2 TIM_Channel_1 TIM_IT_CC1 TIM_GetCapture1 第 502 页 共 928 零死角玩转 STM32—F429 8 #define TPAD_TIM_GPIO_CLK 9 #define TPAD_TIM_CH_PORT 10 #define TPAD_TIM_CH_PIN 11 #define TPAD_TIM_AF 12 #define TPAD_TIM_SOURCE 使用宏定义非常方便程序升级、移植。 RCC_AHB1Periph_GPIOA GPIOA GPIO_Pin_5 GPIO_AF_TIM2 GPIO_PinSource5 开发板选择使用通用定时器 2 的通道 1 连接到电容按键,对应的引脚为 PA5。 定时器初始化配置 代码清单 33-2 定时器初始化配置 1 static void TIMx_CHx_Cap_Init(uint32_t arr,uint16_t psc) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 5 TIM_ICInitTypeDef TIM_ICInitStructure; 6 7 //使能 TIM 时钟 8 RCC_APB1PeriphClockCmd(TPAD_TIM_CLK,ENABLE); 9 //使能通道引脚时钟 10 RCC_AHB1PeriphClockCmd(TPAD_TIM_GPIO_CLK, ENABLE); 11 //指定引脚复用 12 GPIO_PinAFConfig(TPAD_TIM_CH_PORT,TPAD_TIM_SOURCE,TPAD_TIM_AF); 13 14 //端口配置 15 GPIO_InitStructure.GPIO_Pin = TPAD_TIM_CH_PIN; 16 //复用功能 17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 18 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 19 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 20 //不带上下拉 21 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 22 GPIO_Init(TPAD_TIM_CH_PORT, &GPIO_InitStructure); 23 24 //初始化 TIM 25 //设定计数器自动重装值 26 TIM_TimeBaseStructure.TIM_Period = arr; 27 //预分频器 28 TIM_TimeBaseStructure.TIM_Prescaler =psc; 29 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; 30 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; 31 TIM_TimeBaseInit(TPAD_TIMx, &TIM_TimeBaseStructure); 32 //初始化通道 33 //选择定时器输入通道 34 TIM_ICInitStructure.TIM_Channel = TPAD_TIM_Channel_X; 35 //上升沿触发 36 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; 37 // 输入捕获选择 38 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; 39 //配置输入分频,不分频 40 TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; 41 //配置输入滤波器 不滤波 42 TIM_ICInitStructure.TIM_ICFilter = 0x00; 43 TIM_ICInit(TPAD_TIMx, &TIM_ICInitStructure); 44 //使能 TIM 45 TIM_Cmd ( TPAD_TIMx, ENABLE ); 46 } 首先定义三个初始化结构体变量,这三个结构体之前都做了详细的介绍,可以参考相 关章节理解。 使用外设之前都必须开启相关时钟,这里开启定时器时钟和定时器通道引脚对应端口 时钟,并指定定时器通道引脚复用功能。 第 503 页 共 928 零死角玩转 STM32—F429 接下来初始化配置定时器通道引脚为复用功能,无需上下拉。 然后,配置定时器功能。定时器周期和预分频器值由函数形参决定,采用向上计数方 式。指定输入捕获通道,电容按键检测需要采用上升沿触发方式。 最后,使能定时器。 电容按键复位 代码清单 33-3 电容按键复位 1 static void TPAD_Reset(void) 2{ 3 GPIO_InitTypeDef GPIO_InitStructure; 4 5 //配置引脚为普通推挽输出 6 GPIO_InitStructure.GPIO_Pin = TPAD_TIM_CH_PIN; 7 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; 8 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 9 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 10 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_DOWN; 11 GPIO_Init(TPAD_TIM_CH_PORT, &GPIO_InitStructure); 12 13 //输出低电平,放电 14 GPIO_ResetBits ( TPAD_TIM_CH_PORT, TPAD_TIM_CH_PIN ); 15 //保持一小段时间低电平,保证放电完全 16 Delay_ms(5); 17 18 //清除中断标志 19 TIM_ClearITPendingBit(TPAD_TIMx, TPAD_TIM_IT_CCx|TIM_IT_Update); 20 //计数器归 0 21 TIM_SetCounter(TPAD_TIMx,0); 22 23 //引脚配置为复用功能,不上、下拉 24 GPIO_InitStructure.GPIO_Pin = TPAD_TIM_CH_PIN; 25 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; 26 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; 27 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; 28 GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; 29 GPIO_Init(TPAD_TIM_CH_PORT,&GPIO_InitStructure); 30 } 该函数实现两个主要功能:控制电容按键放电和复位计数器。 首先,配置定时器通道引脚作为普通 GPIO,使其为下拉的推挽输出模式。然后调用 GPIO_ResetBits 函数输出低电平,为保证放电完整,需要延时一小会时间,这里调用 Delay_ms 函数完成 5 毫秒的延时。Delay_ms 函数是定义在 bsp_SysTick.h 文件的一个延时 函数,它利用系统滴答定时器功能实现毫秒级的精准延时。也因此要求在调用电容按键检 测相关函数之前必须先初始化系统滴答定时器。 这里还需要一个注意的地方,在控制电容按键放电的整个过程定时器是没有停止的, 计数器还是在不断向上计数的,只是现阶段计数值对我们来说没有意义而已。 然后,清除定时器捕获/比较标志位和更新中断标志位以及将定时器计数值赋值为 0, 使其重新从 0 开始计数。 最后,配置定时器通道引脚为定时器复用功能,不上下拉。在执行完该 GPIO 初始化 函数后,电容按键就马上开始充电,定时器通道引脚电压就上升,当达到 1.8V 时定时器就 输入捕获成功。所以在执行完 TPAD_Reset 函数后应用程序需要不断查询定时器输入捕获 标志,在发送输入捕获时马上读取 TIMx_CCRx 寄存器的值,作为该次电容按键捕获值。 第 504 页 共 928 零死角玩转 STM32—F429 获取输入捕获值 代码清单 33-4 获取输入捕获值 1 //定时器最大计数值 2 #define TPAD_ARR_MAX_VAL 0XFFFF 3 4 static uint16_t TPAD_Get_Val(void) 5{ 6 /* 先放电完全,并复位计数器 */ 7 TPAD_Reset(); 8 //等待捕获上升沿 9 while (TIM_GetFlagStatus ( TPAD_TIMx, TPAD_TIM_IT_CCx ) == RESET) { 10 //超时了,直接返回 CNT 的值 11 if (TIM_GetCounter(TPAD_TIMx)>TPAD_ARR_MAX_VAL-500) 12 return TIM_GetCounter(TPAD_TIMx); 13 }; 14 /* 捕获到上升沿后输出 TIMx_CCRx 寄存器值 */ 15 return TPAD_TIM_GetCaptureX(TPAD_TIMx ); 16 } 开始是 TPAD_ARR_MAX_VAL 的宏定义,它指定定时器自动重载寄存器(TIMx_ARR) 的值。 TPAD_Get_Val 函数用来获取一次电容按键捕获值,包括电容按键放电和输入捕获过 程。 先调用 TPAD_Reset 函数完成电容按键放电过程,并复位计数器。 接下来,使用 TIM_GetFlagStatus 函数获取当前计数器的输入捕获状态,如果成功输入 捕获就使用 TPAD_TIM_GetCaptureX 函数获取此刻定时器捕获/比较寄存器的值并返回该值。 如果还没有发生输入捕获,说明还处于充电过程,就进入等待状态。 为防止无限等待情况,加上超时处理函数,如果发生超时则直接返回计数器值。实际 上,如果发生超时情况,很大可能是硬件出现问题。 获取最大输入捕获值 代码清单 33-5 获取最大输入捕获值 1 static uint16_t TPAD_Get_MaxVal(uint8_t n) 2{ 3 uint16_t temp=0; 4 uint16_t res=0; 5 while (n--) { 6 temp=TPAD_Get_Val();//得到一次值 7 if (temp>res)res=temp; 8 }; 9 return res; 10 } 该函数接收一个参数,用来指定获取电容按键捕获值的循环次数,函数的返回值则为 n 次发生捕获中最大的捕获值。 电容按键捕获初始化 代码清单 33-6 电容按键捕获初始化 1 uint8_t TPAD_Init(void) 2{ 3 uint16_t buf[10]; 4 uint16_t temp; 5 uint8_t j,i; 6 7 //设定定时器预分频器目标时钟为:9MHz(180Mhz/20) 第 505 页 共 928 零死角玩转 STM32—F429 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 TIMx_CHx_Cap_Init(TPAD_ARR_MAX_VAL,20-1); for (i=0; i<10; i++) { //连续读取 10 次 buf[i]=TPAD_Get_Val(); Delay_ms(10); } for (i=0; i<9; i++) { //排序 for (j=i+1; j<10; j++) { if (buf[i]>buf[j]) { //升序排列 temp=buf[i]; buf[i]=buf[j]; buf[j]=temp; } } } temp=0; //取中间的 6 个数据进行平均 for (i=2; i<8; i++) { temp+=buf[i]; } tpad_default_val=temp/6; /* printf 打印函数调试使用,用来确定阈值 TPAD_GATE_VAL,在应用工程中应注释掉 */ printf("tpad_default_val:%d\r\n",tpad_default_val); //初始化遇到超过 TPAD_ARR_MAX_VAL/2 的数值,不正常! if (tpad_default_val>TPAD_ARR_MAX_VAL/2) { return 1; } return 0; 该函数实现定时器初始化配置和无触摸时电容按键捕获值确定功能。它一般在 main 函 数靠前位置调用完成电容按键初始化功能。 程序先调用 TIMx_CHx_Cap_Init 函数完成定时器基本初始化和输入捕获功能配置,两 个参数用于设置定时器的自动重载计数和定时器时钟频率,这里自动重载计数被赋值为 TPAD_ARR_MAX_VAL,这里对该值没有具体要求,不要设置过低即可。定时器时钟配 置设置为 9MHz 为合适,实验中用到 TIM2,默认使用内部时钟为 180MHz,经过参数设置 预分频器为 20 分频,使定时器时钟为 9MHz。 接下来,循环 10 次读取电容按键捕获值,并保存在数组内。TPAD_Init 函数一般在开 机时被调用,所以认为 10 次读取到的捕获值都是无触摸状态下的捕获值。 然后,对 10 个捕获值从小到大排序,取中间 6 个的平均数作为无触摸状态下的参考捕 获值,并保存在 tpad_default_val 变量中,该值对应图 33-4 中的时间 t1。 程序最后会检测 tpad_default_val 变量的合法性。 电容按键状态扫描 代码清单 33-7 电容按键状态扫描 1 //阈值:捕获时间必须大于(tpad_default_val + TPAD_GATE_VAL),才认为是有效触摸. 2 #define TPAD_GATE_VAL 100 3 4 uint8_t TPAD_Scan(uint8_t mode) 5{ 6 //0,可以开始检测;>0,还不能开始检测 7 static uint8_t keyen=0; 8 //扫描结果 9 uint8_t res=0; 第 506 页 共 928 零死角玩转 STM32—F429 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 } //默认采样次数为 3 次 uint8_t sample=3; //捕获值 uint16_t rval; if (mode) { //支持连按的时候,设置采样次数为 6 次 sample=6; //支持连按 keyen=0; } /* 获取当前捕获值(返回 sample 次扫描的最大值) */ rval=TPAD_Get_MaxVal(sample); /* printf 打印函数调试使用,用来确定阈值 TPAD_GATE_VAL,在应用工程中应注释掉 */ printf("scan_rval=%d\n",rval); //大于 tpad_default_val+TPAD_GATE_VAL,且小于 10 倍 tpad_default_val,则有效 if (rval>(tpad_default_val+TPAD_GATE_VAL)&&rval<(10*tpad_default_val)) //keyen==0,有效 if (keyen==0) { res=1; } keyen=3; //至少要再过 3 次之后才能按键有效 } if (keyen) { keyen--; } return res; TPAD_GATE_VAL 用于指定电容按键触摸阈值,当实时捕获值大于该阈值和无触摸捕 获参考值 tpad_default_val 之和时就认为电容按键有触摸,否则认为没有触摸。阈值大小一 般需要通过测试得到,一般做法是通过串口在 TPAD_Init 函数中把 tpad_default_val 值打印 到串口调试助手并记录下来,在 TPAD_Scan 函数中也把实时捕获值打印出来,在运行时触 摸电容按键,获取有触摸时的捕获值,这样两个值对比就可以大概确定 TPAD_GATE_VAL。 TPAD_Scan 函数用来扫描电容按键状态,需要被循环调用,类似独立按键的状态扫描 函数。它有一个形参,用于指定电容按键的工作模式,当为赋值为 1 时,电容按键支持连 续触发,即当一直触摸不松开时,每次运行 TPAD_Scan 函数都会返回电容按键被触摸状态, 直到松开手指,才返回无触摸状态。当参数赋值为 0 时,每次触摸函数只返回一次被触摸 状态,之后就总是返回无触摸状态,除非松开手指再触摸。TPAD_Scan 函数有一个返回值, 用于指示电容按键状态,返回值为 0 表示无触摸,为 1 表示有触摸。 TPAD_Scan 函数主要是调用 TPAD_Get_MaxVal 函数获取当前电容按键捕获值,该值 这里指定在连续触发模式下取 6 次扫描的最大值为当前捕获值,如果是不连续触发只取三 次扫描的最大值。正常情况下,如果无触摸,当前捕获值与捕获参考值相差很小;如果有 触摸,当前捕获值比捕获参考值相差较大,此时捕获值对应图 33-4 的时间 t2。 接下来比较当前捕获值与无触摸捕获参考值和阈值之和的关系,以确定电容按键状态。 这里为增强可靠性,还加了当前捕获值不能超过参考值的 10 倍的限制条件,因为超过 10 倍关系几乎可以认定为出错情况。 第 507 页 共 928 零死角玩转 STM32—F429 主函数 代码清单 33-8 main 函数 1 int main(void) 2{ 3 /* 初始化蜂鸣器 */ 4 Beep_GPIO_Config(); 5 6 /* 初始化调试串口,一般为串口 1 */ 7 Debug_USART_Config(); 8 9 /* 初始化系统滴答定时器 */ 10 SysTick_Init(); 11 12 /* 初始化电容按键 */ 13 TPAD_Init(); 14 15 while (1) { 16 if (TPAD_Scan(0)) { 17 BEEP_ON; 18 Delay_ms(100); 19 BEEP_OFF; 20 } 21 Delay_ms(50); 22 } 23 } 24 主函数分别调用 Beep_GPIO_Config()、Debug_USART_Config()、和 SysTick_Init()完 成蜂鸣器、调试串口和系统滴答定时器参数。 TPAD_Init 函数初始化配置定时器,并获取无触摸时的捕获参考值。 无限循环中调用 TPAD_Scan 函数完成电容按键状态扫描,指定为不连续触发方式。如 果检测到有触摸就让蜂鸣器响 100ms。 33.2.3 下载验证 使用 USB 线连接开发板上的“USB TO UART”接口到电脑,电脑端配置好串口调试 助手参数。编译实验程序并下载到开发板上,程序运行后在串口调试助手可接收到开发板 发过来有关定时器捕获值的参数信息。用手册触摸开发板上电容按键时可以听到蜂鸣器响 一声,移开手指后再触摸,又可以听到响声。 33.3 每课一问 15. 谈谈定时器时钟频率对阈值大小的影响。 第 508 页 共 928 零死角玩转 STM32—F429 第34章 IWDG—独立看门狗 本章参考资料:《STM32F4XX 中文参考手册》IWDG 章节。 学习本章时,配合《STM32F4XX 中文参考手册》IWDG 章节一起阅读,效果会更佳, 特别是涉及到寄存器说明的部分。 34.1 IWDG 简介 STM32 有两个看门狗,一个是独立看门狗另外一个是窗口看门狗,独立看门狗号称宠 物狗,窗口看门狗号称警犬,本章我们主要分析独立看门狗的功能框图和它的应用。独立 看门狗用通俗一点的话来解释就是一个 12 位的递减计数器,当计数器的值从某个值一直减 到 0 的时候,系统就会产生一个复位信号,即 IWDG_RESET。如果在计数没减到 0 之前, 刷新了计数器的值的话,那么就不会产生复位信号,这个动作就是我们经常说的喂狗。看 门狗功能由 VDD 电压域供电,在停止模式和待机模式下仍能工作。 34.2 IWDG 功能框图剖析 图 34-1 IWDG 功能框图 1. ①独立看门狗时钟 独立看门狗的时钟由独立的 RC 振荡器 LSI 提供,即使主时钟发生故障它仍然有效, 非常独立。LSI 的频率一般在 30~60KHZ 之间,根据温度和工作场合会有一定的漂移,我 们一般取 40KHZ,所以独立看门狗的定时时间并一定非常精确,只适用于对时间精度 要求比较低的场合。 第 509 页 共 928 零死角玩转 STM32—F429 2. ②计数器时钟 递减计数器的时钟由 LSI 经过一个 8 位的预分频器得到,我们可以操作预分频器寄存 器 IWDG_PR 来设置分频因子,分频因子可以是:[4,8,16,32,64,128,256,256],计数器时钟 CK_CNT= 40/ 4*2^PRV,一个计数器时钟计数器就减一。 3. ③计数器 独立看门狗的计数器是一个 12 位的递减计数器,最大值为 0XFFF,当计数器减到 0 时, 会产生一个复位信号:IWDG_RESET,让程序重新启动运行,如果在计数器减到 0 之前刷新 了计数器的值的话,就不会产生复位信号,重新刷新计数器值的这个动作我们俗称喂狗。 4. ④重装载寄存器 重装载寄存器是一个 12 位的寄存器,里面装着要刷新到计数器的值,这个值的大小决 定着独立看门狗的溢出时间。超时时间 Tout = (4*2^prv) / 40 * rlv (s) ,prv 是预分频器寄存 器的值,rlv 是重装载寄存器的值。 5. ⑤键寄存器 键寄存器 IWDG_KR 可以说是独立看门狗的一个控制寄存器,主要有三种控制方式, 往这个寄存器写入下面三个不同的值有不同的效果。 表格 34-1 键寄存器取值枚举 键值 0XAAAA 0X5555 0XCCCC 键值作用 把 RLR 的值重装载到 CNT PR 和 RLR 这两个寄存器可写 启动 IWDG 通过写往键寄存器写 0XCCC 来启动看门狗是属于软件启动的方式,一旦独立看门狗 启动,它就关不掉,只有复位才能关掉。 6. ⑥状态寄存器 状态寄存器 SR 只有位 0:PVU 和位 1:RVU 有效,这两位只能由硬件操作,软件操 作不了。RVU:看门狗计数器重装载值更新,硬件置 1 表示重装载值的更新正在进行中, 更新完毕之后由硬件清 0。PVU: 看门狗预分频值更新,硬件置‘1‘指示预分频值的更新正在 进行中,当更新完成后,由硬件清 0。所以只有当 RVU/PVU 等于 0 的时候才可以更新重装 载寄存器/预分频寄存器。 第 510 页 共 928 零死角玩转 STM32—F429 34.3 怎么用 IWDG 独立看门狗一般用来检测和解决由程序引起的故障,比如一个程序正常运行的时间是 50ms,在运行完这个段程序之后紧接着进行喂狗,我们设置独立看门狗的定时溢出时间为 60ms,比我们需要监控的程序 50ms 多一点,如果超过 60ms 还没有喂狗,那就说明我们监 控的程序出故障了,跑飞了,那么就会产生系统复位,让程序重新运行。 34.4 IWDG 超时实验 34.4.1 硬件设计 1、IWDG 一个 2、按键一个 3、LED 一个 IWDG 属于单片机内部资源,不需要外部电路,需要一个外部的按键和 LED,通过按 键来喂狗,喂狗成功 LED 亮,喂狗失败,程序重启,LED 灭一次。 34.4.2 软件设计 我们编写两个 IWDG 驱动文件,bsp_iwdg.h 和 bsp_iwdg.c,用来存放 IWDG 的初始 化配置函数。 1. 代码分析 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 完整的代码请参考本章配套的工程。 IWDG 配置函数 代码 34-1 IWDG 配置函数 1 void IWDG_Config(uint8_t prv ,uint16_t rlv) 2{ 3 // 使能 预分频寄存器 PR 和重装载寄存器 RLR 可写 4 IWDG_WriteAccessCmd( IWDG_WriteAccess_Enable ); 5 6 // 设置预分频器值 7 IWDG_SetPrescaler( prv ); 8 第 511 页 共 928 9 10 11 12 13 14 15 16 17 } 零死角玩转 STM32—F429 // 设置重装载寄存器值 IWDG_SetReload( rlv ); // 把重装载寄存器的值放到计数器中 IWDG_ReloadCounter(); // 使能 IWDG IWDG_Enable(); IWDG 配置函数有两个形参,prv 用来设置预分频的值,取值可以是: 代码 34-2 形参 prv 取值 1 /* 2* 3* 4* 5* 6* 7* 8* 9 */ @arg IWDG_Prescaler_4: @arg IWDG_Prescaler_8: @arg IWDG_Prescaler_16: @arg IWDG_Prescaler_32: @arg IWDG_Prescaler_64: @arg IWDG_Prescaler_128: @arg IWDG_Prescaler_256: IWDG prescaler set to 4 IWDG prescaler set to 8 IWDG prescaler set to 16 IWDG prescaler set to 32 IWDG prescaler set to 64 IWDG prescaler set to 128 IWDG prescaler set to 256 这些宏在 stm32f10x_iwdg.h 中定义,宏展开是 8 位的 16 进制数,具体作用是配置配置 预分频寄存器 IWDG_PR,获得各种分频系数。形参 rlv 用来设置重装载寄存器 IWDG_RLR 的值,取值范围为 0~0XFFF。溢出时间 Tout = prv/40 * rlv (s),prv 可以是 [4,8,16,32,64,128,256]。如果我们需要设置 1s 的超时溢出,prv 可以取 IWDG_Prescaler_64, rlv 取 625,即调用:IWDG_Config(IWDG_Prescaler_64 ,625)。Tout=64/40*625=1s。 喂狗函数 代码 34-3 喂狗函数 1 void IWDG_Feed(void) 2{ 3 // 把重装载寄存器的值放到计数器中,喂狗,防止 IWDG 复位 4 // 当计数器的值减到 0 的时候会产生系统复位 5 IWDG_ReloadCounter(); 6} 主函数 代码 34-4 主函数 1 int main(void) 2{ 3 /* LED 端口初始化 */ 4 LED_GPIO_Config(); 5 6 Delay(0X8FFFFF); 7 8 /* 检查是否为独立看门狗复位 */ 9 if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) != RESET) 10 { 11 /* 独立看门狗复位 */ 12 /* 亮红灯 */ 13 LED_RED; 第 512 页 共 928 零死角玩转 STM32—F429 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 } /* 清除标志 */ RCC_ClearFlag(); /*如果一直不喂狗,会一直复位,加上前面的延时,会看到红灯闪烁 在 1s 时间内喂狗的话,则会持续亮绿灯*/ } else { /*不是独立看门狗复位(可能为上电复位或者手动按键复位之类的) */ /* 亮蓝灯 */ LED_BLUE; } /*初始化按键*/ Key_GPIO_Config(); // IWDG 1s 超时溢出 IWDG_Config(IWDG_Prescaler_64 ,625); //while 部分是我们在项目中具体需要写的代码,这部分的程序可以用独立看门狗来监控 //如果我们知道这部分代码的执行时间,比如是 500ms,那么我们可以设置独立看门狗的 //溢出时间是 600ms,比 500ms 多一点,如果要被监控的程序没有跑飞正常执行的话,那么 //执行完毕之后就会执行喂狗的程序,如果程序跑飞了那程序就会超时,到达不了喂狗的 //程序,此时就会产生系统复位。但是也不排除程序跑飞了又跑回来了,刚好喂狗了, //歪打正着。所以要想更精确的监控程序,可以使用窗口看门狗,窗口看门狗规定必须 //在规定的窗口时间内喂狗。 while (1) { if ( Key_Scan(KEY1_GPIO_PORT,KEY1_PIN) == KEY_ON ) { // 喂狗,如果不喂狗,系统则会复位,复位后亮红灯,如果在 1s // 时间内准时喂狗的话,则会亮绿灯 IWDG_Feed(); //喂狗后亮绿灯 LED_GREEN; } } 主函数中我们初始化好 LED 和按键相关的配置,设置 IWDG 1s 超时溢出之后,进入 while 死循环,通过按键来喂狗,如果喂狗成功,则亮绿灯,如果喂狗失败的话,系统重启, 程序重新执行,当执行到 RCC_GetFlagStatus 函数的时候,则会检测到是 IWDG 复位,然 后让红等亮。如果喂狗一直失败的话,则会一直产生系统复位,加上前面延时的效果,则 会看到红灯一直闪烁。 我们这里是通过按键来模拟一个喂狗程序,真正的项目中则不是这样使用。while 部分 是我们在项目中具体需要写的代码,这部分的程序可以用独立看门狗来监控,如果我们知 道这部分代码的执行时间,比如是 500ms,那么我们可以设置独立看门狗的溢出时间是 510ms,比 500ms 多一点,如果要被监控的程序没有跑飞正常执行的话,那么执行完毕之 后就会执行喂狗的程序,如果程序跑飞了那程序就会超时,到达不了喂狗的程序,此时就 会产生系统复位,但是也不排除程序跑飞了又跑回来了,刚好喂狗了,歪打正着。所以要 想更精确的监控程序,可以使用窗口看门狗,窗口看门狗规定必须在规定的窗口时间内喂 狗,早了不行,晚了也不行。 第 513 页 共 928 零死角玩转 STM32—F429 34.4.3 下载验证 把编译好的程序下载到开发板,在 1s 的时间内通过按键来不断的喂狗,如果喂狗失败, 红灯闪烁。如果一直喂狗成功,则绿灯常亮。 第 514 页 共 928 零死角玩转 STM32—F429 第35章 WWDG—窗口看门狗 本章参考资料:《STM32F4xx 中文参考手册》WWDG 章节。 学习本章时,配合《STM32F4xx 中文参考手册》WWDG 章节一起阅读,效果会更佳, 特别是涉及到寄存器说明的部分。 35.1 WWDG 简介 STM32 有两个看门狗,一个是独立看门狗,一个是窗口看门狗。我们知道独立看门狗 的工作原理就是一个递减计数器不断的往下递减计数,当减到 0 之前如果没有喂狗的话, 产生复位。窗口看门狗跟独立看门狗一样,也是一个递减计数器不断的往下递减计数,当 减到一个固定值 0X40 时还不喂狗的话,产生复位,这个值叫窗口的下限,是固定的值, 不能改变。这个是跟独立看门狗类似的地方,不同的地方是窗口看门狗的计数器的值在减 到某一个数之前喂狗的话也会产生复位,这个值叫窗口的上限,上限值由用户独立设置。 窗口看门狗计数器的值必须在上窗口和下窗口之间才可以喂狗,这就是窗口看门狗中窗口 两个字的含义 图 35-1 IWDG 与 WWDG 区别 RLR 是重装载寄存器,用来设置独立看门狗的计数器的值。TR 是窗口看门狗的计数 器的值,由用户独立设置,WR 是窗口看门狗的上窗口值,由用户独立设置。 第 515 页 共 928 零死角玩转 STM32—F429 35.2 WWDG 功能框图剖析 图 35-2 窗口看门狗功能框图 1. ①窗口看门狗时钟 窗口看门狗时钟来自 PCLK1,PCLK1 最大是 45M,由 RCC 时钟控制器开启。 2. ②计数器时钟 计数器时钟由 CK 计时器时钟经过预分频器分频得到,分频系数由配置寄存器 CFR 的 位 8:7 WDGTB[1:0]配置,可以是[0,1,2,3],其中 CK 计时器时钟=PCLK1/4096,除以 4096 是手册规定的,没有为什么。所以计数器的时钟 CNT_CK=PCLK1/4096/(2^WDGTB), 这就可以算出计数器减一个数的时间 T= 1/CNT_CK = Tpclk1 * 4096 * (2^WDGTB)。 3. ③计数器 窗口看门狗的计数器是一个递减计数器,共有 7 位,其值存在控制寄存器 CR 的位 6:0, 即 T[6:0],当 7 个位全部为 1 时是 0X7F,这个是最大值,当递减到 T6 位变成 0 时,即从 0X40 变为 0X3F 时候,会产生看门狗复位。这个值 0X40 是看门狗能够递减到的最小值, 所以计数器的值只能是:0X40~0X7F 之间,实际上用来计数的是 T[5:0]。当递减计数器递 减到 0X40 的时候,还不会马上产生复位,如果使能了提前唤醒中断:CFR 位 9 EWI 置 1, 则产生提前唤醒中断,如果真进入了这个中断的话,就说明程序肯定是出问题了, 那么在中断服务程序里面我们就需要做最重要的工作,比如保存重要数据,或者报警等, 这个中断我们也叫它死前中断。 第 516 页 共 928 零死角玩转 STM32—F429 4. ④窗口值 我们知道窗口看门狗必须在计数器的值在一个范围内才可以喂狗,其中下窗口的值是 固定的 0X40,上窗口的值可以改变,具体的由配置寄存器 CFR 的位 6:0 W[6:0]设置。其值 必须大于 0X40,如果小鱼或者等于 0X40 就是失去了窗口的价值,而且也不能大于计数器 的值,所以必须得小于 0X7F。那窗口值具体要设置成多大?这个得根据我们需要监控的程 序的运行时间来决定。如果我们要监控的程序段 A 运行的时间为 Ta,当执行完这段程序之 后就要进行喂狗,如果在窗口时间内没有喂狗的话,那程序就肯定是出问题了。一般计数 器的值 TR 设置成最大 0X7F,窗口值为 WR,计数器减一个数的时间为 T,那么时间: (TR-WR)*T 应该稍微大于 Ta 即可,这样就能做到刚执行完程序段 A 之后喂狗,起到监控 的作用,这样也就可以算出 WR 的值是多少。 第 517 页 共 928 零死角玩转 STM32—F429 5. ⑤计算看门狗超时时间 图 35-3 窗口看门狗时序图 这个图来自数据手册,从图我们知道看门狗超时时间:Twwdg = Tpclk1 x 4096 x 2^wdgtb x (T[5:0] + 1) ms,当 PCLK1 = 30MHZ 时,WDGTB 取不同的值时有最小和最大的 超时时间,那这个最小和最大的超时时间该怎么理解,又是怎么算出来的? 讲起来有点绕, 这里我稍微讲解下 WDGTB=0 时是怎么算的。递减计数器有 7 位 T[6:0] ,当位 6 变为 0 的 时候就会产生复位,实际上有效的计数位是 T[5:0],而且 T6 必须先设置为 1。如果 T[5:0]=0 时,递减计数器再减一次,就产生复位了,那这减一的时间就等于计数器的周期 =1/CNT_CK = Tpclk1 * 4096 * (2^WDGTB) = 1/30 * 4096 *2^0 = 136.53us,这个就是最短的 第 518 页 共 928 零死角玩转 STM32—F429 超时时间。如果 T[5:0]全部装满为 1,即 63,当他减到 0X40 变成 0X3F 时,所需的时间就 是最大的超时时间=113.7*2^5=136.53*64=8.74ms。同理,当 WDGTB 等于 1/2/3 时,代入 公式即可。 35.3 怎么用 WWDG WWDG 一般被用来监测,由外部干扰或不可预见的逻辑条件造成的应用程序背离正 常的运行序列而产生的软件故障。比如一个程序段正常运行的时间是 50ms,在运行完 这个段程序之后紧接着进行喂狗,如果在规定的时间窗口内还没有喂狗,那就说明我 们监控的程序出故障了,跑飞了,那么就会产生系统复位,让程序重新运行。 35.4 WWDG 喂狗实验 35.4.1 硬件设计 4、WWDG 一个 5、LED 两个 WWDG 属于单片机内部资源,不需要外部电路,需要两个 LED 来指示程序的运行状 态。 35.4.2 软件设计 我们编写两个 WWDG 驱动文件,bsp_wwdg.h 和 bsp_wwdg.c,用来存放 WWDG 的初 始化配置函数。 1. 代码分析 这里只讲解核心的部分代码,有些变量的设置,头文件的包含等并没有涉及到, 完整的代码请参考本章配套的工程。 WWDG 配置函数 代码 35-1 WWDG 配置函数 1 /* WWDG 配置函数 2 * tr :递减计时器的值, 取值范围为:0x7f~0x40 3 * wr :窗口值,取值范围为:0x7f~0x40 4 * prv:预分频器值,取值可以是 第 519 页 共 928 零死角玩转 STM32—F429 5* @arg WWDG_Prescaler_1: WWDG counter clock = (PCLK1/4096)/1 6* @arg WWDG_Prescaler_2: WWDG counter clock = (PCLK1/4096)/2 7* @arg WWDG_Prescaler_4: WWDG counter clock = (PCLK1/4096)/4 8* @arg WWDG_Prescaler_8: WWDG counter clock = (PCLK1/4096)/8 9 */ 10 void WWDG_Config(uint8_t tr, uint8_t wr, uint32_t prv) 11 { 12 // 开启 WWDG 时钟 13 RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); 14 15 // 设置递减计数器的值 16 WWDG_SetCounter( tr ); 17 18 // 设置预分频器的值 19 WWDG_SetPrescaler( prv ); 20 21 // 设置上窗口值 22 WWDG_SetWindowValue( wr ); 23 24 // 设置计数器的值,使能 WWDG 25 WWDG_Enable(WWDG_CNT); 26 27 // 清除提前唤醒中断标志位 28 WWDG_ClearFlag(); 29 // 配置 WWDG 中断优先级 30 WWDG_NVIC_Config(); 31 // 开 WWDG 中断 32 WWDG_EnableIT(); 33 } WWDG 配置函数有三个形参,tr 是计数器的值,一般我们设置成最大 0X7F,wr 是上 窗口的值,这个我们要根据监控的程序的运行时间来设置,但是值必须在 0X40 和计数器 的值之间,prv 用来设置预分频的值,取值可以是: 代码 35-2 形参 prv 取值 1 /* 2* 3* 4* 5* 6 */ @arg WWDG_Prescaler_1: WWDG counter clock = (PCLK1/4096)/1 @arg WWDG_Prescaler_2: WWDG counter clock = (PCLK1/4096)/2 @arg WWDG_Prescaler_4: WWDG counter clock = (PCLK1/4096)/4 @arg WWDG_Prescaler_8: WWDG counter clock = (PCLK1/4096)/8 这些宏在 stm32f10x_wwdg.h 中定义,宏展开是 32 位的 16 进制数,具体作用是设置配 置寄存器 CFR 的位 8:7 WDGTB[1:0],获得各种分频系数。 WWDG 中断优先级函数 1 // WWDG 中断优先级初始化 2 static void WWDG_NVIC_Config(void) 3{ 4 NVIC_InitTypeDef NVIC_InitStructure; 5 6 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); 7 NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn; 8 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; 9 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; 10 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; 11 NVIC_Init(&NVIC_InitStructure); 12 } 第 520 页 共 928 零死角玩转 STM32—F429 在递减计数器减到 0X40 的时候,我们开启了提前唤醒中断,这个中断我们称它为死 前中断或者叫遗嘱中断,在中断函数里面我们应该出来最重要的事情,而且必须得快,因 为递减计数器再减一次,就会产生系统复位。 提前唤醒中断复位程序 代码 35-3 提前唤醒中断服务程序 1 // WWDG 中断复服务程序,如果发生了此中断,表示程序已经出现了故障, 2 // 这是一个死前中断。在此中断服务程序中应该干最重要的事, 3 // 比如保存重要的数据等,这个时间具体有多长,要 4 // 由 WDGTB 的值决定: 5 // WDGTB:0 113us 6 // WDGTB:1 227us 7 // WDGTB:2 455us 8 // WDGTB:3 910us 9 void WWDG_IRQHandler(void) 10 { 11 // 清除中断标志位 12 WWDG_ClearFlag(); 13 14 //LED2 亮,点亮 LED 只是示意性的操作, 15 //真正使用的时候,这里应该是做最重要的事情 16 LED2(ON); 17 } 喂狗函数 代码 35-4 喂狗函数 1 // 喂狗 2 void WWDG_Feed(void) 3{ 4 // 喂狗,刷新递减计数器的值,设置成最大 WDG_CNT=0X7F 5 WWDG_SetCounter( WWDG_CNT ); 6} 喂狗就是重新刷新递减计数器的值防止系统复位,喂狗一般是在主函数中喂。 主函数 代码 35-5 主函数 1 int main(void) 2{ 3 uint8_t wwdg_tr, wwdg_wr; 4 5 /* LED 端口初始化 */ 6 LED_GPIO_Config(); 7 8 // BLUE 蓝色灯亮 9 LED3(ON); 10 Delay(0XFFFFFF); 11 12 // WWDG 配置 13 14 /* WWDG 配置函数 15 * tr :递减计时器的值, 取值范围为:0x7f~0x40,超出范围会直接复位 第 521 页 共 928 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 } 零死角玩转 STM32—F429 * wr :窗口值,取值范围为:0x7f~0x40 * prv:预分频器值,取值可以是 * @arg WWDG_Prescaler_1: WWDG counter clock = (PCLK1(45MHz)/4096)/1 约 10968Hz 91us * @arg WWDG_Prescaler_2: WWDG counter clock = (PCLK1(45MHz)/4096)/2 约 5484Hz 182us * @arg WWDG_Prescaler_4: WWDG counter clock = (PCLK1(45MHz)/4096)/4 约 2742Hz 364us * @arg WWDG_Prescaler_8: WWDG counter clock = (PCLK1(45MHz)/4096)/8 约 1371Hz 728us * * 例:tr = 127(0x7f,tr 的最大值) * wr = 80(0x50, 0x40 为最小 wr 最小值) * prv = WWDG_Prescaler_8 * 窗口时间为 728 * (127-80) = 34.2ms < 刷新窗口 < ~728 * 64 = 46.6ms * 也就是说调用 WWDG_Config 进行这样的配置,若在之后的 34.2ms 前喂狗, * 系统会复位,在 46.6ms 后没有喂狗,系统也会复位。 * 需要在刷新窗口的时间内喂狗,系统才不会复位。 */ // 初始化 WWDG:配置计数器初始值,配置上窗口值,启动 WWDG,使能提前唤醒中断 WWDG_Config(127,80,WWDG_Prescaler_8); // 窗口值我们在初始化的时候设置成 0X5F,这个值不会改变 wwdg_wr = WWDG->CFR & 0X7F; while (1) { // BLUE 蓝色灯 LED3(OFF); //----------------------------------------------------// 这部分应该写需要被 WWDG 监控的程序,这段程序运行的时间 // 决定了窗口值应该设置成多大。 //----------------------------------------------------- // 计时器值,初始化成最大 0X7F,当开启 WWDG 时候,这个值会不断减小 // 当计数器的值大于窗口值时喂狗的话,会复位,当计数器减少到 0X40 // 还没有喂狗的话就非常非常危险了,计数器再减一次到了 0X3F 时就复位 // 所以要当计数器的值在窗口值和 0X40 之间的时候喂狗,其中 0X40 是固定的。 wwdg_tr = WWDG->CR & 0X7F; if ( wwdg_tr < wwdg_wr ) { // 喂狗,重新设置计数器的值为最大 0X7F WWDG_Feed(); } } 主函数中我们把 WWDG 的计数器的值设置 为 0X7F,上窗口值设置为 0X5F,分频系 数为 8 分频,则计数器减 1 的时间约为 728us。在 while 死循环中,我们不断读取计数器的 值,当计数器的值减小到小于上窗口值的时候,我们喂狗,让计数器重新计数。 在 while 死循环中,一般是我们需要监控的程序,这部分代码的运行时间,决定了上 窗口值应该设置为多少,当监控的程序运行完毕之后,我们需要执行喂狗程序,比起独立 看门狗,这个喂狗的窗口时间是非常短的,对时间要求很精确。如果没有在这个窗口时间 内喂狗的话,那就说明程序出故障了,会产生提前唤醒中断,最后系统复位。 第 522 页 共 928 零死角玩转 STM32—F429 35.4.3 下载验证 把编译好的程序下载到开发板,LED3 被点亮,一段时间之后熄灭,之后 LED3 一直就 没有被点亮过,说明系统没有产生复位,如果产生复位的话 LED3 会再被点亮一次。中断 服务程序中的 LED 也没被点亮过,说明喂狗正常。 第 523 页 共 928 零死角玩转 STM32—F429 第36章 SDIO—SD 卡读写测试 本章参考资料:《STM32F4xx 参考手册》、《STM32F4xx 规格书》、库帮助文档 《stm32f4xx_dsp_stdperiph_lib_um.chm》以及 SD 简易规格文件《Physical Layer Simplified Specification V2.0》(版本号:2.00)。 特别说明,本书内容是以 STM32F42x 系列控制器资源讲解。 阅读本章内容之前,建议先阅读 SD 简易规格文件。 36.1 SDIO 简介 SD 卡(Secure Digital Memory Card)在我们生活中已经非常普遍了,控制器对 SD 卡进行 读写通信操作一般有两种通信接口可选,一种是 SPI 接口,另外一种就是 SDIO 接口。 SDIO 全称是安全数字输入/输出接口,多媒体卡(MMC)、SD 卡、SD I/O 卡都有 SDIO 接口。 STM32F42x 系列控制器有一个 SDIO 主机接口,它可以与 MMC 卡、SD 卡、SD I/O 卡以及 CE-ATA 设备进行数据传输。MMC 卡可以说是 SD 卡的前身,现阶段已经用得很少。SD I/O 卡本身不是用于存储的卡,它是利用 SDIO 传输协议的一种外设。比如 Wi-Fi Card,它 主要是提供 Wi-Fi 功能,可能有些 Wi-Fi 模块是使用串口或者 SPI 接口进行通信的,但 WiFi Card 是使用 SDIO 接口进行通信的。并且一般设计 SD I/O 卡是可以插入到 SD 的插槽的。 CE-ATA 是专为轻薄笔记本硬盘设计的硬盘高速通讯接口。 多媒体卡协会网站 www.mmca.org 中提供了有 MMCA 技术委员会发布的多媒体卡系统 规范。 SD 卡协会网站 www.sdcard.org 中提供了 SD 存储卡和 SDIO 卡系统规范。 CE-ATA 工作组网站 www.ce-ata.org 中提供了 CE_ATA 系统规范。 随之科技发展,SD 卡容量需求越来越大,SD 卡发展到现在也是有几个版本的,关于 SDIO 接口的设备整体概括见图 36-1。 第 524 页 共 928 零死角玩转 STM32—F429 图 36-1 SDIO 接口的设备 关于 SD 卡和 SD I/O 部分内容可以在 SD 协会网站获取到详细的介绍,比如各种 SD 卡 尺寸规则、读写速度标示方法、应用扩展等等信息。 本章内容针对 SD 卡使用讲解,对于其他类型卡的应用可以参考相关系统规范实现, 所以对于控制器中针对其他类型卡的内容可能在本章中简单提及或者被忽略,本章内容不 区分 SDIO 和 SD 卡这两个概念。即使目前 SD 协议提供的 SD 卡规范版本最新是 4.01 版本, 但 STM32F42x 系列控制器只支持 SD 卡规范版本 2.0,即只支持标准容量 SD 和高容量 SDHC 标准卡,不支持超大容量 SDXC 标准卡,所以可以支持的最高卡容量是 32GB。 36.2 SD 卡物理结构 一张 SD 卡包括有存储单元、存储单元接口、电源检测、卡及接口控制器和接口驱动 器 5 个部分,见图 36-2。存储单元是存储数据部件,存储单元通过存储单元接口与卡控制 单元进行数据传输。电源检测控制 SD 卡工作在合适的电压下,如果出现掉电或上电情况 控制控制单元和存储单元接口复位。卡及接口控制单元控制 SD 卡运行状态,它包括有 8 个寄存器。接口驱动器控制 SD 卡引脚输入输出。 第 525 页 共 928 零死角玩转 STM32—F429 图 36-2 SD 卡物理结构 SD 卡总共有 8 个寄存器,用于设定或标示 SD 卡信息,参考表 36-1。这些寄存器只能 通过对应的命令访问,对 SD 卡进行控制操作并不是像操作控制器 GPIO 相关寄存器一样一 次读写一个寄存器,它是通过命令来控制的,SDIO 定义 64 个命令,每个命令都有特殊意 义,可以实现某一特定功能,程序员只需要组合发送命令就可以实现 SD 卡控制、读操作、 写操作。SD 卡控制命令的执行,进而才会读写 SD 卡寄存器内容。 表 36-1 SD 卡寄存器 名称 bit 宽度 描述 CID 128 卡识别号(Card identification number):用来识别的卡的个体号码(唯 一的) RCA 16 相对地址(Relative card address):卡的本地系统地址,初始化时,动 态地由卡建议,主机核准。 DSR 16 驱动级寄存器(Driver Stage Register):配置卡的输出驱动 CSD 128 卡的特定数据(Card Specific Data):卡的操作条件信息 SCR 64 SD 配置寄存器(SD Configuration Register):SD 卡特殊特性信息 OCR 32 操作条件寄存器(Operation conditions register) SSR 512 SD 状态(SD Status):SD 卡专有特征的信息 CSR 32 卡状态(Card Status):卡状态信息 第 526 页 共 928 零死角玩转 STM32—F429 每个寄存器位的含义可以参考 SD 简易规格文件《Physical Layer Simplified Specification V2.0》第 5 章内容。 36.3 SDIO 总线 36.3.1 总线拓扑 SD 卡一般都支持 SDIO 和 SPI 两个读写操作,本章内容只介绍 SDIO 接口操作方式, 如果需要使用 SPI 操作方式可以参考 SPI 相关章节。另外,STM32F42x 系列控制器的 SDIO 是不支持 SPI 通信模式的,如果需要用到 SPI 通信只能使用 SPI 外设。 SD 卡总线拓扑参考图 36-3。不推荐多卡槽共用总线信号,要求一个单独 SD 总线应该 连接一个单独的 SD 卡。 图 36-3 SD 卡总线拓扑 SD 卡使用 9-pin 接口通信,其中 3 根电源线、1 根时钟线、1 根命令线和 4 根数据线, 具体说明如下: CLK:时钟线,由 SDIO 主机产生,即由 STM32 控制器输出; CMD:命令控制线,SDIO 主机通过该线发送命令控制 SD 卡,如果命令要求 SD 卡提 供应答,SD 卡也是通过该线传输应答信息; D0-3:数据线,传输读写数据;SD 卡可将 D0 拉低标示忙状态; VDD、VSS1、VSS2:电源和地信号。 在之前的 I2C 以及 SPI 章节都有详细讲解了对应的通信时序,实际上,SDIO 的通信时 序简单许多,SDIO 不管是从主机控制器向 SD 卡传输,还是 SD 卡向主机控制器传输都只 是以 CLK 时钟线的上升沿为有效。SD 卡操作过程会有两个时钟频率,一个是识别卡阶段 时钟频率 FOD,最高为 400kHz,另外一个是数据传输模式下时钟频率 FPP,默认最高为 25MHz,如果通过相关寄存器配置使 SDIO 工作在高速模式,此时数据传输模式最高频率 为 50MHz。 第 527 页 共 928 零死角玩转 STM32—F429 对于 STM32 控制器只有一个 SDIO 主机,所以只能连接一个 SDIO 设备,开发板上集 成了一个 Micro SD 卡槽和 SDIO 接口的 WiFi 模块,要求只能使用其中一个设备。SDIO 接 口的 WiFi 模块一般集成有使能线,如果需要用到 SD 卡需要先控制该使能线禁用 WiFi 模 块。 36.3.2 总线协议 SD 总线通信是基于命令和数据传输。由一个起始位(“0”),由一个停止位(“1”)终 止。SD 通信一般是主机发送一个命令(Command),从设备在接收到命令后作出响应 (Response),如有需要会数据(Data)传输参与。 SD 总线的基本交互是命令与响应交互,见图 36-4。 图 36-4 命令与响应交互 数据是以块(Black)形式传输,SDHC 卡数据块长度一般为 512 字节,数据可以从主机 到卡,也可以是从卡到主机。数据块需要 CRC 位来保证数据传输成功。CRC 位由 SD 卡系 统硬件生成。STM32 控制器可以控制使用单线或 4 线传输,开发板设计使用 4 线传输。图 36-5 为主机向 SD 卡写入数据块操作示意。 图 36-5 多块写入操作 单块写入和多块写入是由对应的命令来启动的,多块写入还需要使用命令来停止整个 写入操作。数据写入需要检测 SD 卡忙状态,因为 SD 卡在接收到数据后编程到存储区过程 需要一定操作时间。SD 卡忙状态通过把 D0 线拉低表示。 数据块读操作与之类似,只是无需忙状态检测。 第 528 页 共 928 零死角玩转 STM32—F429 使用 4 数据线传输时,每次传输 4bit 数据,每根数据线都必须有起始位、终止位以及 CRC 位,CRC 位每根数据线都要分别检查,并把检查结果汇总然后在数据传输完后通过 D0 线反馈给主机。 SD 卡数据包有两种格式,一种是常规数据(8bit 宽),它是先发低字节再发高字节,而 每个字节则是先发高位再发低位,4 线传输示意如图 36-6。 图 36-6 8 位宽数据包传输 4 线同步发送,每根线发送一个字节的其中两个位,数据位在四线顺序排列发送, DAT3 数据线发较高位,DAT0 数据线发较低位。 另外一种数据包发送格式是宽位数据包格式,对 SD 卡而言宽位数据包发送方式是针 对 SD 卡 SSR(SD 状态)寄存器内容发送的,SSR 寄存器总共有 512bit,在主机发出 ACMD13 命令后 SD 卡将 SSR 寄存器内容通过 DAT 线发送给主机。宽位数据包格式示意 见图 36-7。 36.3.3 命令 图 36-7 宽位数据包传输 命令由 SD 主机发出,可以是广播命令或者寻址命令,广播命令是针对同个 SD 主机的 所有从设备发送,寻址命令是指定某个地址设备进行命令传输。因为一般一个 SDIO 主机 只接一个 SD 卡,所有广播命令实际上也就只有 SD 卡响应。 第 529 页 共 928 零死角玩转 STM32—F429 1. 命令格式 SD 命令格式固定为 48bit,都是通过 CMD 线连续传输的,见图 36-8。 图 36-8 SD 命令格式 起始位和终止位都是固定不变的。对于命令,传输标志为 1,是从主机传输到 SD 卡; 对于响应,传输标志为 0,是从 SD 卡传输到主机。命令内容包括命令、地址信息/参数和 CRC 校验三个部分。命令固定占用 6bit,所以总共有 64 个命令(代号:COM0~COM63), 每个命令都有特定的用途,部分命令不适用与 SD 卡操作,而是专门用于 MMC 卡或者 SD I/O 卡。每个命令有 32bit 地址信息/参数用于命令附加内容,对于广播命令没有地址信息, 这 32bit 用于指定参数,对于寻址命令这 32bit 用于指定目标 SD 卡的地址。7bit 校验位用于 验证命令传输内容正确性,如果发生外部干扰导致传输数据个别位状态改变将导致校准失 败,也意味着命令传输失败,SD 卡不执行命令。 2. 命令类型 命令可以 4 种类型:  无响应广播命令(bc),发送到所有卡,不返回任务响应;  带响应广播命令(bcr),发送到所有卡,同时接收来自所有卡响应;  寻址命令(ac),发送到选定卡,DAT 线无数据传输;  寻址数据传输命令(adtc),发送到选定卡,DAT 线有数据传输。 另外,SD 卡主机模块系统旨在为各种应用程序类型提供一个标准接口。在此环境中, 需要有特定的客户/应用程序功能。为实现这些功能,在标准中定义了两种类型的通用命令: 特定应用命令(ACMD)和常规命令(GEN_CMD)。要使用 SD 卡制造商特定的 ACMD 比如 ACMD6,需要在发送该命令之前发送 CMD55 命令,告知 SD 卡接下来的命令为特定应用 命令。CMD55 命令只对紧接的第一个命令有效,如果检查发现是 ACMD 命令就执行特定 应用功能,如果检测发现不是 ACMD 命令,执行标准命令。 3. 命令描述 SD 卡系统的命令被分为多个类,每个类支持一种“卡的功能设置”。表 36-2 列举了 SD 卡部分命令信息,更多详细信息可以参考 SD 简易规格文件说明,表中填充位和保留位 都必须被设置为 0。 第 530 页 共 928 零死角玩转 STM32—F429 虽然没有必须完全记住每个命令详细信息,但越熟悉命令对后面编程理解非常有帮助。 命令 序号 CMD0 CMD2 CMD3 CMD4 CMD7 CMD8 CMD9 CMD10 CMD12 CMD13 CMD15 CMD16 CMD17 CMD18 CMD24 CMD25 CMD27 CMD32 CMD33 CMD38 CMD42 CMD55 CMD56 类 型 bc bcr bcr bc ac bcr ac ac ac ac ac ac adtc adtc adtc adtc adtc ac ac ac adtc ac adtc 参数 [31:0]填充位 [31:0]填充位 [31:0]填充位 [31:16]DSR[15:0] 填充位 [31:16]RCA[15:0] 填充位 [31:12] 保 留 位 [11:8]VHS[7:0]检 查模式 [31:16]RCA[15:0] 填充位 [31:16]RCA[15:0] 填充位 [31:0]填充位 [31:16]RCA[15:0] 填充位 [31:16]RCA[15:0] 填充位 [31:0]块长度 [31:0]数据地址 [31:0]数据地址 [31:0]数据地址 [31:0]数据地址 [31:0]填充位 [31:0]数据地址 [31:0]数据地址 [31:0]填充位 [31:0]保留 [31:16]RCA[15:0] 填充位 [31:1] 填 充 位 [0] 读/写 表 36-2 SD 部分命令描述 响 应 缩写 描述 基本命令(Class 0) - GO_IDLE_STATE R2 ALL_SEND_CID R6 SEND_RELATIVE_ADDR 复位所有的卡到 idle 状态。 通 知 所有 卡 通 过 CMD 线 返回 CID 值。 通知所有卡发布新 RCA。 - SET_DSR 编程所有卡的 DSR。 R1b SELECT/DESELECT_CARD 选择/取消选择 RCA 地址卡。 R7 SEND_IF_COND 发送 SD 卡接口条件,包含主机支持 的电压信息,并询问卡是否支持。 R2 SEND_CSD 选定卡通过 CMD 线发送 CSD 内容 R2 SEND_CID 选定卡通过 CMD 线发送 CID 内容 R1b STOP_TRANSMISSION R1 SEND_STATUS 强制卡停止传输 选定卡通过 CMD 线发送它状态寄存 器 - GO_INACTIVE_STATE 使选定卡进入“inactive”状态 面向块的读操作(Class 2) R1 SET_BLOCK_LEN R1 READ_SINGLE_BLOCK R1 READ_MULTIPLE_BLOCK 面向块的写操作(Class 4) R1 WRITE_BLOCK R1 WRITE_MILTIPLE_BLOCK R1 PROGRAM_CSD 擦除命令(Class 5) R1 ERASE_WR_BLK_START R1 ERASE_WR_BLK_END R1b ERASE 加锁命令(Class 7) R1 LOCK_UNLOCK 特定应用命令(Class 8) R1 APP_CMD R1 GEN_CMD SD 卡特定应用命令 对于标准 SD 卡,设置块命令的长 度,对于 SDHC 卡块命令长度固定为 512 字节。 对于标准卡,读取 SEL_BLOCK_LEN 长度字节的块;对于 SDHC 卡,读取 512 字节的块。 连续从 SD 卡读取数据块,直到被 CMD12 中断。块长度同 CMD17。 对于标准卡,写入 SEL_BLOCK_LEN 长度字节的块;对于 SDHC 卡,写入 512 字节的块。 连续向 SD 卡写入数据块,直到被 CMD12 中断。每块长度同 CMD17。 对 CSD 的可编程位进行编程 设置擦除的起始块地址 设置擦除的结束块地址 擦除预先选定的块 加锁/解锁 SD 卡 指定下个命令为特定应用命令,不 是标准命令 通用命令,或者特定应用命令中, 用于传输一个数据块,最低位为 1 表示读数据,为 0 表示写数据 第 531 页 共 928 零死角玩转 STM32—F429 ACMD6 ac [31:2] 填 充 位 [1:0]总线宽度 R1 ACMD13 adtc [31:0]填充位 R1 [32] 保 留 位 [30]HCS(OCR[30] ACMD41 Bcr ) [29:24]保留位 R3 [23:0]VDD 电 压 (OCR[23:0]) ACMD51 adtc [31:0]填充位 R1 SET_BUS_WIDTH SD_STATUS SD_SEND_OP_COND SEND_SCR 定义数据总线宽度 ('00'=1bit,'10'=4bit)。 发送 SD 状态 主机要求卡发送它的支持信息(HCS) 和 OCR 寄存器内容。 读取配置寄存器 SCR 36.3.4 响应 响应由 SD 卡向主机发出,部分命令要求 SD 卡对命令作出响应,很多用于反馈 SD 卡 的状态。SDIO 总共有 7 个响应类型(代号:R1~R7),特定的命令会得到其中一个的响应内 容。比如当主机发送 COM3 命令时,可以得到响应 R6。SD 卡没有 R4、R5 类型响应。SD 卡响应也是通过 CMD 线连续传输的。根据响应内容大小可以非常短响应和长响应。短响 应是 48bit 长度,只有 R2 类型是长响应,其长度为 136bit。各个类型响应具体情况如表 36-3。 除了 R3 类型之外,其他响应都使用 CRC7 校验来保护,对于 R2 类型是使用 CID 和 CSD 寄存器内部 CRC7。 表 36-3 SD 卡响应类型 描述 Bit 位宽 值 备注 R1(正常响应命令) 起始位 传输位 命令号 卡状态 47 46 [45:40] [39:8] 1 1 6 32 "0" "0" x x 如果有传输到卡的数据,那么在数据线可能有 busy 信号 R2(CID,CSD 寄存器) CRC7 [7:1] 7 x 终止位 0 1 "1" 描述 Bit 位宽 值 备注 起始位 传输位 保留 [127:1] 135 134 [133:128] 127 1 1 6 x "0" "0" "111111" CID 或者 CSD 寄存器[127:1]位的值 CID 寄存器内容作为 CMD2 和 CMD10 响应,CSD 寄存器内容作为 CMD9 响应。 R3(OCR 寄存器) 终止位 0 1 "1" 描述 Bit 位宽 值 备注 起始位 传输位 保留 OCR 寄存器 47 46 [45:40] [39:8] 1 1 6 32 "0" "0" "111111" x OCR 寄存器的值作为 ACMD41 的响应 R6(发布的 RCA 寄存器响应) 保留 [7:1] 7 "1111111" 终止位 0 1 "1" 描述 Bit 位宽 值 备注 起始位 传输位 CMD3 RCA 寄存器 卡状态位 47 46 [45:40] [39:8] 1 1 6 16 16 "0" "0" "000011" x x 专用于命令 CMD3 的响应 R7(发布的 RCA 寄存器响应) CRC7 [7:1] 7 x 终止位 0 1 "1" 第 532 页 共 928 零死角玩转 STM32—F429 描述 Bit 位宽 值 备注 起始位 传输位 CMD8 保留 接收电压 检测模式 47 46 [45:40] [39:20] [19:16] [15:8] 1 1 6 20 4 8 "0" "0" "001000" "00000h" x x 专用于命令 CMD8 的响应,返回卡支持电压范围和检测模式 36.4 SD 卡功能 CRC7 [7:1] 7 x 终止位 0 1 "1" 36.4.1 SD 卡操作模式 SD 卡有多个多个版本,STM32 控制器目前最高支持《Physical Layer Simplified Specification V2.0》定义的 SD 卡,STM32 控制器对 SD 卡进行数据读写之前需要识别卡的 种类:V1.0 标准卡、V2.0 标准卡、V2.0 高容量卡或者不被识别卡。 SD 卡系统(包括主机和 SD 卡)定义了两种操作模式:卡识别模式和数据传输模式。在 系统复位后,主机处于卡识别模式,寻找总线上可用的 SDIO 设备;SD 卡也会处于卡识别 模式,直到被主机识别到即卡接收到 SEND_RCA(CMD3)命令,之后 SD 卡就会进入数据 传输模式,而主机会在总线上所有卡被识别后也进入数据传输模式。在每个操作模式下, SD 卡都有几种状态,参考表 36-4,通过命令控制实现卡状态的切换。 表 36-4 SD 卡状态与操作模式 操作模式 无效模式(Inactive) 卡识别模式(Card identification mode) 数据传输模式(Data transfer mode) SD 卡状态 无效状态(Inactive State) 空闲状态(Idle State) 准备状态(Ready State) 识别状态(Identification State) 待机状态(Stand-by State) 传输状态(Transfer State) 发送数据状态(Sending-data State) 接收数据状态(Receive-data State) 编程状态(Programming State) 断开连接状态(Disconnect State) 36.4.2 卡识别模式 在卡识别模式下,主机会复位所有处于“卡识别模式”的 SD 卡,确认其工作电压范 围,识别 SD 卡类型,并且要求 SD 卡发布相对卡地址(Relative Card Address)。在卡识别过 程中,卡应该在识别时钟频率 FOD 下的 SD 时钟频率中工作。 主机上电后,所有卡处于空闲状态,包括当前处于无效状态的卡。主机也可以发送 GO_IDLE_STATE(CMD0)让所有卡软复位从而进入空闲状态,但当前处于无效状态的卡并 不会复位。 主机在开始要与卡通信时,需要先确定双方在互相支持的电压范围内。卡有一个支持 电压范围,主机当前电压必须在该范围可能才能正常与卡通信。SEND_IF_COND(CMD8) 就是用于验证卡接口操作条件(主要是电压支持)。卡会根据命令的参数来检测操作条件匹 第 533 页 共 928 零死角玩转 STM32—F429 配性,如果卡支持主机电压就产生响应,否则不发送。而主机则根据响应内容确定卡的电 压匹配性。CMD8 是 SD 卡标准 V2.0 版本才有的新命令,所以如果主机有接收到响应,可 以判断卡为 V2.0 或更高版本 SD 卡。 SD_SEND_OP_COND(ACMD41)命令可以实现主机识别和拒绝不匹配它的电压范围的 卡。ACMD41 命令的 VDD 电压参数用于设置主机支持电压范围,卡响应会返回卡支持的 电压范围。对于对 CMD8 有相应的卡,把 ACMD41 命令的 HCS 位设置为 1,可以测试卡 的容量类型,如果卡响应的 CCS 位为 1 说明为高容量 SD 卡,否则为标准卡。卡在响应 ACMD41 之后进入准备状态,不响应 ACMD41 的卡为不可用卡,进入无效状态。 ACMD41 是应用特定命令,发送该命令之前必须先发 CMD55。 卡识别模式下 SD 卡状态转换如图 36-9。 图 36-9 卡识别模式状态转换图 ALL_SEND_CID(CMD2)用来命令所有卡返回它们的卡识别号(CID)。处于准备状态的 卡在发送 CID 之后就进入识别状态。之后主机就发送 SEND_RELATIVE_ADDR(CMD3)命 令给卡,让卡自己推荐一个相对地址(RCA)并响应命令。这个 RCA 是 16bit 地址,而 CID 是 128bit 地址,使用 RCA 简化通信。卡在接收到 CMD3 并发出响应后就进入数据传输模 式,并处于待机状态,主机在获取所有卡 RCA 之后也进入数据传输模式。 第 534 页 共 928 零死角玩转 STM32—F429 36.4.3 数据传输模式 只有 SD 卡系统处于数据传输模式下才可以进行数据读写操作。数据传输模式下可以 将主机 SD 时钟频率设置为 FPP,默认最高为 25MHz,频率切换可以通过 CMD4 命令来实 现。数据传输模式下,SD 卡状态转换过程见图 36-10。CMD7 用来选定和取消指定的卡, 卡在待机状态下还不能进行数据通信,因为总线上可能有多个卡都是出于待机状态,必须 选择一个 RCA 地址目标卡使其进入传输状态才可以进行数据通信。同时通过 CMD7 命令 也可以让已经被选择的目标卡返回到待机状态。 数据传输模式下的数据通信都是主机和目标卡之间通过寻址命令点对点进行的。卡处 于传输状态下就可以使用表 36-2 中面向块的读操作、写操作以及擦除命令对卡进行数据读 写、擦除。CMD12 可以中断正在进行的数据通信,让卡返回到传输状态。CMD0 和 CMD15 会中止任何数据编程操作,返回卡识别模式,这可能导致卡数据被损坏。 图 36-10 数据传输模式卡状态转换 36.5 SDIO 功能框图 STM32 控制器有一个 SDIO,由两部分组成:SDIO 适配器和 APB2 接口,见图 36-11。 SDIO 适配器提供 SDIO 主机功能,可以提供 SD 时钟、发送命令和进行数据传输。APB2 接口用于控制器访问 SDIO 适配器寄存器并且可以产生中断和 DMA 请求信号。 第 535 页 共 928 零死角玩转 STM32—F429 图 36-11 SDIO 功能框图 SDIO 使用两个时钟信号,一个是 SDIO 适配器时钟(SDIOCLK=48MHz),另外一个是 APB2 总线时钟(PCLK2,一般为 90MHz)。 STM32 控制器的 SDIO 是针对 MMC 卡和 SD 卡的主设备,所以预留有 8 根数据线, 对于 SD 卡最多用四根数据线。 1. SDIO 适配器 SDIO 适配器是 SD 卡系统主机部分,是 STM32 控制器与 SD 卡数据通信中间设备。 SDIO 适配器由五个单元组成,见图 36-12。 图 36-12 SDIO 适配器框图 2. 控制单元 控制单元包含电源管理和时钟管理功能,结构如图 36-13。电源管理部件会在系统断 电和上电阶段禁止 SD 卡总线输出信号。时钟管理部件控制 CLK 线时钟信号生成。一般使 用 SDIOCLK 分频得到。 第 536 页 共 928 零死角玩转 STM32—F429 图 36-13 SDIO 适配器控制单元 3. 命令路径 命令路径控制命令发送,并接收卡的响应,结构见图 36-14。 图 36-14 SDIO 适配器命令路径 关于 SDIO 适配器状态转换流程可以参考图 36-9,虽然图中 SD 卡的状态转换但当 SD 卡处于某一状态时,SDIO 适配器必然处于特定状态与之对应。STM32 控制器以命令路径 状态机(CPSM)来描述 SDIO 适配器状态变化情况。并加入了等待超时检测功能,以便退出 永久等待情况。 4. 数据路径 数据路径部件负责与 SD 卡相互数据传输,内部结构见图 36-15。 第 537 页 共 928 零死角玩转 STM32—F429 图 36-15 SDIO 适配器数据路径 SD 卡系统数据传输状态转换参考图 36-10,SDIO 适配器以数据路径状态机(DPSM)来 描述 SDIO 适配器状态变化情况。并加入了等待超时检测功能,以便退出永久等待情况。 发送数据时,DPSM 处于等待发送(Wait_S)状态,如果数据 FIFO 不为空,DPSM 变成发送 状态并且数据路径部件启动向卡发送数据。接收数据是,DPSM 处于等待接收状态,在 DPSM 收到起始位时变成接收状态,并且数据路径部件开始从卡接收数据。 5. 数据 FIFO 数据 FIFO(先进先出)部件是一个数据缓冲器,带发送和接收单元。控制器的 FIFO 包 含宽度为 32bit、深度为 32 字的数据缓冲器和发送/接收逻辑。SDIO 状态寄存器 (SDIO_STA)的 TXACT 位用于指示当前正在传输数据,RXACT 位指示当前正在接收数据。 这两个位不可能同时为 1。当 TXACT 为 1 时,可以通过 APB2 接口将数据写入到传输 FIFO。当 RXACT 为 1 时,接收 FIFO 存放从数据路径部件接收到的数据。根据 FIFO 空或 满状态会把 SDIO_STA 寄存器位值 1,并可以产生中断和 DMA 请求。 36.6 SDIO 初始化结构体详解 标准库函数对 SDIO 外设建立了三个初始化结构体,分别为 SDIO 初始化结构体 SDIO_InitTypeDef、SDIO 命令初始化结构体 SDIO_CmdInitTypeDef 和 SDIO 数据初始化结 构体 SDIO_DataInitTypeDef。初始化结构体成员用于设置 SDIO 工作环境参数,并由 SDIO 相应初始化配置函数或功能函数调用,这些设定参数将会设置 SDIO 相应的寄存器,达到 配置 SDIO 工作环境的目的。 初始化结构体和初始化库函数配合使用是标准库精髓所在,理解了初始化结构体每个 成员意义基本上就可以对该外设运用自如了。初始化结构体定义在 stm32f4xx_sdio.h 文件 中,初始化库函数定义在 stm32f4xx_sdio.c 文件中,编程时我们可以结合这两个文件内注 释使用。 SDIO 初始化结构体用于配置 SDIO 基本工作环境,比如时钟分频、时钟沿、数据宽度 等等。它被 SDIO_Init 函数使用。 代码清单 36-1 SDIO 初始化结构体 1 typedef struct { 第 538 页 共 928 零死角玩转 STM32—F429 2 uint32_t SDIO_ClockEdge; 3 uint32_t SDIO_ClockBypass; 4 uint32_t SDIO_ClockPowerSave; 5 uint32_t SDIO_BusWide; 6 uint32_t SDIO_HardwareFlowControl; 7 uint8_t SDIO_ClockDiv; 8 } SDIO_InitTypeDef; // 时钟沿 // 旁路时钟 // 节能模式 // 数据宽度 // 硬件流控制 // 时钟分频 (9) SDIO_ClockEdge:主时钟 SDIOCLK 产生 CLK 引脚时钟有效沿选择,可选上升沿或下 降沿,它设定 SDIO 时钟控制寄存器(SDIO_CLKCR)的 NEGEDGE 位的值,一般选择 设置为高电平。 (10) SDIO_ClockBypass:时钟分频旁路使用,可选使能或禁用,它设定 SDIO_CLKCR 寄 存器的 BYPASS 位。如果使能旁路,SDIOCLK 直接驱动 CLK 线输出时钟;如果禁用, 使用 SDIO_CLKCR 寄存器的 CLKDIV 位值分频 SDIOCLK,然后输出到 CLK 线。一 般选择禁用时钟分频旁路。 (11) SDIO_ClockPowerSave:节能模式选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存 器的 PWRSAV 位的值。如果使能节能模式,CLK 线只有在总线激活是才有时钟输出; 如果禁用节能模式,始终使能 CLK 线输出时钟。 (12) SDIO_BusWide:数据线宽度选择,可选 1 位数据总线、4 位数据总线或 8 为数据总线, 系统默认使用 1 位数据总线,操作 SD 卡时在数据传输模式下一般选择 4 位数据总线。 它设定 SDIO_CLKCR 寄存器的 WIDBUS 位的值。 (13) SDIO_HardwareFlowControl:硬件流控制选择,可选使能或禁用,它设定 SDIO_CLKCR 寄存器的 HWFC_EN 位的值。硬件流控制功能可以避免 FIFO 发送上溢 和下溢错误。 (14) SDIO_ClockDiv:时钟分频系数,它设定 SDIO_CLKCR 寄存器的 CLKDIV 位的值, 设置 SDIOCLK 与 CLK 线输出时钟分频系数: CLK 线时钟频率=SDIOCLK/([CLKDIV+2])。 SDIO 命令初始化结构体用于设置