datasheet
超过460,000+ 应用技术资源下载
pdf

STM32学习资料

  • 1星
  • 日期: 2015-05-15
  • 大小: 19.31MB
  • 所需积分:3分
  • 下载次数:0
  • favicon收藏
  • rep举报
  • 分享
  • free评论
标签: STM32

正点原子的STM32学习资料,库函数版本,非常经典。

STM32 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 不完全手册 V3.0(库函数版) −ALIENTEK MiniSTM32 开发板教程 官方店铺 1:http://shop62103354.taobao.com 官方店铺 2:http://shop62057469.taobao.com 技术论坛:www.openedv.com I STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 内容简介 ........................................................................................................................ I 前言 ................................................................................................................................2 第一篇 硬件篇 ..............................................................................................................4 第一章 实验平台简介 ..................................................................................................5 1.1 ALIENTEK MiniSTM32 开发板资源初探 ......................................................... 5 1.2 ALIENTEK MiniSTM32 开发板资源说明.......................................................... 7 1.2.1 硬件资源说明 .................................................................................................... 7 1.2.2 软件资源说明 .................................................................................................. 10 1.3 ALIENTEK MiniSTM32 V3.0 开发板升级说明............................................... 12 第二章 实验平台硬件资源详解 ................................................................................13 2.1 开发板原理图详解 ............................................................................................ 13 2.1.1 MCU ................................................................................................................. 13 2.1.2 EEPROM .......................................................................................................... 15 2.1.3 温度传感器 ..................................................................................................... 15 2.1.4 按键 ................................................................................................................. 15 2.1.5 液晶显示模块 ................................................................................................. 16 2.1.6 红外接收头 ..................................................................................................... 16 2.1.7 PS/2 接口.......................................................................................................... 17 2.1.8 LED .................................................................................................................. 17 2.1.9 SD 卡 ................................................................................................................ 18 2.1.10 无线模块 ....................................................................................................... 18 2.1.11 SPI FLASH ..................................................................................................... 19 2.1.12 USB 串口、USB、电源................................................................................ 19 2.2 开发板使用注意事项 ........................................................................................ 20 2.3 STM32 学习方法 ................................................................................................ 21 第二篇 软件篇 ............................................................................................................23 第三章 MDK5 软件入门............................................................................................24 3.1 STM32 官方固件库简介 .................................................................................... 24 3.1.1 库开发与寄存器开发的关系 ........................................................................ 24 3.1.2 STM32 固件库与 CMSIS 标准讲解 ............................................................. 25 3.1.3 STM32 官方库包介绍 ................................................................................... 26 II STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 3.1.3.1 文件夹介绍: .............................................................................................. 27 3.1.3.2 关键文件介绍: .......................................................................................... 28 3.2MDK5 简介.......................................................................................................... 29 3.3 新建基于 V3.5 固件库的 MDK5 工程模板 ..................................................... 30 3.3.1 MDK5 安装步骤.............................................................................................. 31 3.3.2 添加 License Key ............................................................................................. 31 3.3.3 新建工程模板 .................................................................................................. 33 3.4 程序下载与调试 ................................................................................................ 54 3.4.1 STM32 软件仿真 ............................................................................................. 54 3.4.2 STM32 程序下载 ............................................................................................. 62 3.4.3 JLINK 下载与调试程序 .................................................................................. 67 3.5 MDK5 使用技巧................................................................................................. 72 3.5.1 文本美化 ......................................................................................................... 72 3.5.2 语法检测&代码提示 ...................................................................................... 74 3.5.3 代码编辑技巧 ................................................................................................. 76 3.5.4 其他小技巧 ..................................................................................................... 80 第四章 STM32 开发基础知识入门 ............................................................................82 4.1 MDK 下 C 语言基础复习 .................................................................................. 82 4.1.1 位操作 ............................................................................................................. 82 4.1.2 define 宏定义 ................................................................................................... 83 4.1.3 ifdef 条件编译.................................................................................................. 83 4.1.4 extern 变量申明 ............................................................................................... 84 4.1.5 typedef 类型别名 ............................................................................................. 84 4.1.6 结构体 ............................................................................................................. 85 4.2 STM32 系统架构 ................................................................................................ 87 4.3 STM32 时钟系统 ................................................................................................ 88 4.4 端口复用和重映射 ............................................................................................ 92 4.4.1 端口复用功能 .................................................................................................. 92 4.4.2 端口重映射 ...................................................................................................... 93 4.5 STM32 NVIC 中断优先级管理 ......................................................................... 94 4.6 MDK 中寄存器地址名称映射分析................................................................... 97 4.7 MDK 固件库快速组织代码技巧..................................................................... 100 第五章 SYSTEM 文件夹介绍 .................................................................................105 III STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 5.1 delay 文件夹代码介绍 ..................................................................................... 105 5.1.1 delay_init 函数 ............................................................................................... 106 5.1.2 delay_us 函数 ................................................................................................. 107 5.1.3 delay_ms 函数................................................................................................ 109 5.2 sys 文件夹代码介绍 ......................................................................................... 110 5.2.1 IO 口的位操作实现 .................................................................................... 110 5.2.2 中断分组设置函数 ....................................................................................... 112 5.3 usart 文件夹介绍 .............................................................................................. 112 5.3.1 printf 函数支持 .............................................................................................. 113 5.3.2 uart_init 函数.................................................................................................. 113 5.3.3 USART1_IRQHandler 函数........................................................................... 116 第三篇 实战篇 ..........................................................................................................119 第六章 跑马灯实验 ..................................................................................................120 6.1 STM32 IO 简介................................................................................................. 121 6.2 硬件设计 .......................................................................................................... 128 6.3 软件设计 .......................................................................................................... 129 6.4 仿真与下载 ...................................................................................................... 138 第七章 按键输入实验 ..............................................................................................141 7.1 STM32 IO 口简介............................................................................................. 142 7.2 硬件设计 .......................................................................................................... 142 7.3 软件设计 .......................................................................................................... 142 7.4 仿真与下载 ...................................................................................................... 146 第八章 串口实验 ......................................................................................................151 8.1 STM32 串口简介 .............................................................................................. 152 8.2 硬件设计 .......................................................................................................... 155 8.3 软件设计 .......................................................................................................... 155 8.4 下载验证 .......................................................................................................... 159 第九章 外部中断实验 ..............................................................................................161 9.1 STM32 外部中断简介 ...................................................................................... 162 9.2 硬件设计 .......................................................................................................... 165 9.3 软件设计 .......................................................................................................... 165 9.4 下载验证 .......................................................................................................... 168 第十章 独立看门狗(IWDG)实验 .......................................................................170 IV STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 10.1 STM32 独立看门狗简介 ................................................................................ 171 10.2 硬件设计 ........................................................................................................ 173 10.3 软件设计 ........................................................................................................ 173 10.4 下载验证 ........................................................................................................ 174 第十一章 窗口门狗(WWDG)实验.....................................................................176 11.1 STM32 窗口看门狗简介 ................................................................................ 177 11.2 硬件设计 ........................................................................................................ 179 11.3 软件设计 ........................................................................................................ 179 11.4 下载验证 ........................................................................................................ 182 第十二章 定时器中断实验 ......................................................................................183 12.1 STM32 通用定时器简介 ................................................................................ 184 12.2 硬件设计 ........................................................................................................ 189 12.3 软件设计 ........................................................................................................ 189 12.4 下载验证 ........................................................................................................ 191 第十三章 PWM 输出实验........................................................................................192 13.1 PWM 简介....................................................................................................... 193 13.2 硬件设计 ........................................................................................................ 196 13.3 软件设计 ........................................................................................................ 196 13.4 下载验证 ........................................................................................................ 198 第十四章 输入捕获实验 ..........................................................................................199 14.1 输入捕获简介 ................................................................................................ 200 14.2 硬件设计 ........................................................................................................ 204 14.3 软件设计 ........................................................................................................ 204 14.4 下载验证 ........................................................................................................ 208 第十五章 OLED 显示实验 ......................................................................................210 15.1 OLED 简介 ..................................................................................................... 211 15.2 硬件设计 ........................................................................................................ 217 15.3 软件设计 ........................................................................................................ 218 15.4 下载验证 ........................................................................................................ 225 第十六章 TFTLCD 显示实验 ..................................................................................227 16.1 TFTLCD 简介 ................................................................................................. 228 16.2 硬件设计 ........................................................................................................ 234 16.3 软件设计 ........................................................................................................ 234 V STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 16.4 下载验证 ........................................................................................................ 246 第十七章 USMART 调试组件实验.........................................................................247 17.1 USMART 调试组件简介................................................................................ 248 17.2 硬件设计 ........................................................................................................ 252 17.3 软件设计 ........................................................................................................ 252 17.4 下载验证 ........................................................................................................ 256 第十八章 RTC 实时时钟实验..................................................................................260 18.1 STM32 RTC 时钟简介 ................................................................................... 261 18.2 硬件设计 ........................................................................................................ 267 18.3 软件设计 ........................................................................................................ 267 18.4 下载验证 ........................................................................................................ 274 第十九章 待机唤醒实验 ..........................................................................................276 19.1 STM32 待机模式简介 .................................................................................... 277 19.2 硬件设计 ........................................................................................................ 280 19.3 软件设计 ........................................................................................................ 280 19.4 下载与测试 .................................................................................................... 283 第二十章 ADC 实验.................................................................................................284 20.1 STM32 ADC 简介 .......................................................................................... 285 20.2 硬件设计 ........................................................................................................ 293 20.3 软件设计 ........................................................................................................ 293 20.4 下载验证 ........................................................................................................ 295 第二十一章 内部温度传感器实验 ..........................................................................297 21.1 STM32 内部温度传感器简介 ....................................................................... 298 21.2 硬件设计 ........................................................................................................ 298 21.3 软件设计 ........................................................................................................ 298 21.4 下载验证 ........................................................................................................ 300 第二十二章 DAC 实验.............................................................................................302 22.1 STM32 DAC 简介 .......................................................................................... 303 22.2 硬件设计 ........................................................................................................ 307 22.3 软件设计 ........................................................................................................ 308 22.4 下载验证 ........................................................................................................ 311 第二十三章 DMA 实验............................................................................................312 VI STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 23.1 STM32 DMA 简介.......................................................................................... 313 23.2 硬件设计 ........................................................................................................ 318 23.3 软件设计 ........................................................................................................ 318 23.4 下载验证 ........................................................................................................ 321 第二十四章 IIC 实验................................................................................................323 24.1 IIC 简介........................................................................................................... 324 24.2 硬件设计 ........................................................................................................ 324 24.3 软件设计 ........................................................................................................ 325 24.4 下载验证 ........................................................................................................ 332 第二十五章 SPI 实验 ..............................................................................................334 25.1 SPI 简介 ......................................................................................................... 335 25.2 硬件设计 ........................................................................................................ 338 25.3 软件设计 ........................................................................................................ 339 25.4 下载验证 ........................................................................................................ 344 第二十六章 触摸屏实验 ..........................................................................................346 26.1 触摸屏简介 .................................................................................................... 347 26.1.1 电阻式触摸屏 .............................................................................................. 347 26.1.2 电容式触摸屏 .............................................................................................. 347 26.2 硬件设计 ........................................................................................................ 350 26.3 软件设计 ........................................................................................................ 351 26.4 下载验证 ........................................................................................................ 363 第二十七章 红外遥控实验 ....................................................................................365 27.1 红外遥控简介 ................................................................................................ 366 27.2 硬件设计 ........................................................................................................ 367 27.3 软件设计 ........................................................................................................ 367 27.4 下载验证 ........................................................................................................ 373 第二十八章 DS18B20 数字温度传感器实验 .......................................................375 28.1 DS18B20 简介 ................................................................................................ 376 28.2 硬件设计 ........................................................................................................ 377 28.3 软件设计 ........................................................................................................ 378 28.4 下载验证 ........................................................................................................ 382 第二十九章 无线通信实验 ....................................................................................384 29.1 NRF24L01 无线模块简介 .............................................................................. 385 VII STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 29.2 硬件设计 ........................................................................................................ 385 29.3 软件设计 ........................................................................................................ 386 29.4 下载验证 ........................................................................................................ 394 第三十章 PS2 鼠标实验.........................................................................................396 30.1 PS/2 简介......................................................................................................... 397 30.2 硬件设计 ........................................................................................................ 399 30.3 软件设计 ........................................................................................................ 400 30.4 下载验证 ........................................................................................................ 409 第三十一章 FLASH 模拟 EEPROM 实验 ..............................................................411 31.1 STM32 FLASH 简介 ...................................................................................... 412 31.2 硬件设计 ........................................................................................................ 418 31.3 软件设计 ........................................................................................................ 418 31.4 下载验证 ........................................................................................................ 422 第三十二章 内存管理实验 ......................................................................................424 32.1 内存管理简介 ................................................................................................ 425 32.2 硬件设计 ........................................................................................................ 426 32.3 软件设计 ........................................................................................................ 426 32.4 下载验证 ........................................................................................................ 432 第三十三章 SD 卡实验 ..........................................................................................434 33.1 SD 卡简介 ....................................................................................................... 435 33.2 硬件设计 ........................................................................................................ 437 33.3 软件设计 ........................................................................................................ 438 33.4 下载验证 ........................................................................................................ 443 第三十四章 FATFS 实验........................................................................................445 34.1 FATFS 简介 ..................................................................................................... 446 34.2 硬件设计 ........................................................................................................ 451 34.3 软件设计 ........................................................................................................ 451 34.4 下载验证 ........................................................................................................ 458 第三十五章 汉字显示实验 ......................................................................................460 35.1 汉字显示原理简介 ........................................................................................ 461 35.2 硬件设计 ........................................................................................................ 465 35.3 软件设计 ........................................................................................................ 465 35.4 下载验证 ........................................................................................................ 474 VIII STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三十六章 图片显示实验 ......................................................................................477 36.1 图片格式简介 ................................................................................................ 478 36.2 硬件设计 ........................................................................................................ 479 36.3 软件设计 ........................................................................................................ 480 36.4 下载验证 ........................................................................................................ 488 第三十七章 串口 IAP 实验......................................................................................490 37.1 IAP 简介.......................................................................................................... 491 37.2 硬件设计 ........................................................................................................ 497 37.3 软件设计 ........................................................................................................ 497 37.4 下载验证 ........................................................................................................ 502 第三十八章 触控 USB 鼠标实验 ............................................................................504 38.1 USB 简介 ........................................................................................................ 505 38.2 硬件设计 ........................................................................................................ 507 38.3 软件设计 ........................................................................................................ 507 38.4 下载验证 ........................................................................................................ 512 第三十九章 USB 读卡器实验 .................................................................................514 39.1 USB 读卡器简介 ............................................................................................ 515 39.2 硬件设计 ........................................................................................................ 515 39.3 软件设计 ........................................................................................................ 516 39.4 下载验证 ........................................................................................................ 519 第四十章 UCOSII 实验 1-任务调度........................................................................521 40.1 UCOSII 简介................................................................................................... 522 40.2 硬件设计 ........................................................................................................ 526 40.3 软件设计 ........................................................................................................ 527 40.4 下载验证 ........................................................................................................ 530 第四十一章 UCOSII 实验 2-信号量和邮箱............................................................531 41.1 UCOSII 信号量和邮箱简介........................................................................... 532 41.2 硬件设计 ........................................................................................................ 534 41.3 软件设计 ........................................................................................................ 534 41.4 下载验证 ........................................................................................................ 539 第四十二章 UCOSII 实验 3-消息队列、信号量集和软件定时器........................541 42.1 UCOSII 消息队列、信号量集和软件定时器简介....................................... 542 IX STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 42.2 硬件设计 ........................................................................................................ 549 42.3 软件设计 ........................................................................................................ 549 42.4 下载验证 ........................................................................................................ 557 第四十三章 MiniSTM32 开发板综合实验 .............................................................558 43.1 MiniSTM32 开发板综合实验简介 ................................................................ 559 43.2 MiniSTM32 开发板综合实验详解 ................................................................ 559 43.2.1 电子图书 ..................................................................................................... 562 43.2.2 数码相框 ..................................................................................................... 564 43.2.3 USB 连接 ..................................................................................................... 565 43.2.4 应用中心 ..................................................................................................... 566 43.2.5 时钟 ............................................................................................................. 568 43.2.6 系统设置 ..................................................................................................... 568 43.2.7 画板 ............................................................................................................. 574 43.2.8 无线传书 ..................................................................................................... 576 43.2.9 记事本 ......................................................................................................... 577 X STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 内容简介 本手册将由浅入深,带领大家进入 STM32 的世界。本手册总共分为三篇:1,硬件篇,主 要介绍我们的实验平台;2,软件篇,主要介绍 STM32 开发软件的使用以及一些下载调试的技 巧,并详细介绍了几个常用的系统文件(程序);3,实战篇,主要通过 38 个实例(绝大部分是 直接操作 V3.5 版本库函数完成的)带领大家一步步深入 STM32 的学习。 本手册为 ALIENTEK MiniSTM32 V3.0 开发板的配套教程,在开发板配套的光盘里面,有 详细原理图以及所有实例的完整代码,这些代码都有详细的注释,所有源码都经过我们严格测 试,不会有任何警告和错误,另外,源码有我们生成好的 hex 文件,大家只需要通过串口下载 到开发板即可看到实验现象,亲自体验实验过程。 本手册不仅非常适合广大学生和电子爱好者学习 STM32,其大量的实验以及详细的解说, 也是公司产品开发的不二参考。 I STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 前言 Cortex-M3 采用目前主流 ARM V7-M 架构,相比曾风靡一时的 ARMV4T 架构拥有更加强 劲的性能,更高的代码密度,更高的性价比。Cortex-M3 处理器结合多种突破性技术, 在低功耗、低成本、高性能三方面具有突破性的创新,使其在这几年迅速在中低端单片 机市场异军突起。 国内 Cortex-M3 市场,ST(意法半导体)公司的 STM32 无疑是最大赢家,作为 Cortex-M3 内核最先尝蟹的两个公司(另一个是 Luminary(流明))之一,ST 无论是在市场占有率,还 是在技术支持方面,都是远超其他对手。在 Cortex-M3 芯片的选择上,STM32 无疑是大家的首 选。所以自从 ST 推出 STM32 之后,一股强劲的 STM32 学习开发风潮扑面而来。本书也因 STM32 的流行应运而生。 本手册结合《STM32 参考手册》,《Cortex-M3 权威指南》以及《固件库中文参考手册》三 者的优点,通过对关键寄存器以及相关固件库函数的讲解,深入浅出,向读者展示 STM32 的 各种功能。总共配有 38 个实例,基本上每个实例在均配有软硬件设计,在介绍完软硬件之后, 马上附上实例代码,并带有详细注释及说明,让读者快速理解 STM32 各个外设固件库函数含 义以及实例代码运行过程。 这些实例涵盖了 STM32 的绝大部分内部资源,并且提供很多实用级别的程序,如:内存 管理、文件系统、图片解码、IAP 等。所有实例在 MDK5.10 编译器下编译通过,大家只需下载 程序到 ALIENTEK miniSTM32 开发板 V3.0,即可验证实验。 不管你是一个 STM32 初学者,还是一个老手,这本手册都非常适合。尤其对于初学者, 我们将手把手的教你如何使用 MDK,包括新建工程、编译、仿真、下载调试等一系列步骤, 让你轻松上手。 本手册参考的实验平台是 ALIENTEK miniSTM32 开发板 V3.0,有这款开发板的朋友则直 接可以拿配套光盘上的例程在开发板上运行、验证。而没有这款开发板而又想要的朋友,可以 上淘宝购买。当然你如果有了一款自己的开发板,而又不想再买,也是可以的,只要你的板子 上有 ALIENTEK miniSTM32 开发板上的相同资源(需要实验用到的),代码一般都是可以通 用的,你需要做的就只是把外设的驱动函数(一般是 IO 操作)稍做修改,使之适合你的开发 板即可。 本手册分为库函数版本和寄存器版本,本手册为其库函数版本,需要仔细学习 STM32 底 层寄存器开发的朋友可以直接参考我们的寄存器版本手册。 在这里我们要提到 STM32 寄存器开发和固件库开发的区别。寄存器开发跟传统的 51,AVR 单片机开发类似,直接操作底层寄存器。开发人员需要掌握相关寄存器的作用,这对于直接从 51,AVR 单片机转型而来的开发人员来说,他们更加熟悉底层原理,所以偏向寄存器开发,因为 这样他们的代码更加高速有效,内存利用率更高。但是 STM32 寄存器纷繁复杂,不便于快速 开发项目,所以 ST 推出了一整套固件库,将底层寄存器开发全部封装成库函数,这样使得开 发人员摆脱直接操作寄存器的烦恼,直接操作库函数开发自己的程序,项目开发更加快速,但 是内存利用率稍低。 简而言之,库函数开发用牺牲程序效率换取开发速度,而寄存器开发牺牲开发速度换取程 序运行效率。所以针对两类不同的用户人群不同的需求,我们书籍提供两个版本。 两版本的内容大致接近,只是讲解的侧重点不一样,库函数版本的侧重点是 STM32 的固 2 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 件库原理以及使用固件库实现所有实例,寄存器则更加侧重寄存器的讲解以及使用寄存器实现 所有例程。两版本的实战篇的实验列表基本一致,有兴趣的朋友可以对比阅读,加深理解。 俗话说:人无完人。本手册也不例外,在编写过程中虽然得到了不少网友的指正,但难免 不再有出错的地方,如果大家发现本书中有什么错误的地方,还请告诉本人一声,本人邮箱: xingyidianzi@foxmail.com,也可以去 www.openedv.com 论坛给我们留言。在此先向各位朋友表 示真心的感谢。 3 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第一篇 硬件篇 实践出真知,要想学好 STM32,实验平台必不可少!本篇将详细介绍我们用来学习 STM32 的硬件平台:ALIENTEK MiniSTM32 开发板,通过该篇的介绍,读者将了解到我们的学习平台 ALIENTEK MiniSTM32 开发板的功能及特点。 为了让读者更好的使用 ALIENTEK MiniSTM32 开发板,本篇还介绍了开发板的一些使用 注意事项,请读者在使用开发板的时候一定要注意。 本篇将分为如下两章: 1,实验平台简介; 2,实验平台硬件资源详解; 4 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第一章 实验平台简介 本章,主要向大家简要介绍我们的实验平台:ALIENTEK MiniSTM32 开发板。通过本章的 学习,你将对我们后面使用的实验平台有个大概了解,为后面的学习做铺垫。 本章将分为如下两节: 1.1,ALIENTEK MiniSTM32 开发板资源初探; 1.2,ALIENTEK MiniSTM32 开发板资源说明; 1.1 ALIENTEK MiniSTM32 开发板资源初探 ALIENTEK MiniSTM32 开发板是一款迷你型的 STM32F103 开发板,小巧而不小气,简约 而不简单。该开发板自推出以来,深得广大 STM32 学习者喜爱,总销量超过 1.6W 套。目前最 新版本为 V3.0,最新 MiniSTM32 开发板资源图如图 1.1.1 所示: 红外&温度 GPIOA 传感器连接口 引出 IO 口 OLED&LCD 共用接口 STM32F GPIOB&C 103RCT6 引出 IO 口 HS0038 红外接收头 DS18B20 预留接口 USB 串口/ 串口 1 2 个 LED 灯 STM32 USB 口 24C02 EEPROM JTAG/SWD CH340G USB 转 串口接口 STM3 启动 配置选择 电源 指示灯 复位 按键 WK_UP 两个普通 按键 按键 图 1.1.1 MiniSTM32 开发板资源图 NRF24L01 模块接口 W25Q64 64M FLASH SD 卡 接 口 (在背面) GPIOC&D 引出 IO 口 5V 电源输 出/输入 3.3V 电源 输出/输入 PS/2 鼠标/ 键盘接口 电源 开关 电源 芯片 5 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 这款 MiniSTM32 V3.0 开发板,设计精良,结构小巧!板子的设计充分考虑了成本与功能 这两个矛盾面,再结合实际使用的经验及 STM32 的特点,最终确定了这样的设计。总体来说 是该有的都有,不该有的坚决不要,可有可无的选择性价比最高的留下。 ALIENTEK MiniSTM32 开发板板载资源如下: ◆ CPU:STM32F103RCT6,LQFP64,FLASH:256K,SRAM:48K; ◆ 1 个标准的 JTAG/SWD 调试下载口 ◆ 1 个电源指示灯(蓝色) ◆ 2 个状态指示灯(DS0:红色,DS1:绿色) ◆ 1 个红外接收头,配备一款小巧的红外遥控器 ◆ 1 个 IIC 接口的 EEPROM 芯片,24C02,容量 256 字节 ◆ 1 个 SPI FLASH 芯片,W25Q64,容量为 8M 字节(即 64M bit) ◆ 1 个 DS18B20/DS1820 温度传感器预留接口 ◆ 1 个标准的 2.4/2.8/3.5/4.3/7 寸 LCD 接口,支持触摸屏 ◆ 1 个 OLED 模块接口(与 LCD 接口部分共用) ◆ 1 个 USB 串口接口,可用于程序下载和代码调试 ◆ 1 个 USB SLAVE 接口,用于 USB 通信 ◆ 1 个 SD 卡接口 ◆ 1 个 PS/2 接口,可外接鼠标、键盘 ◆ 1 组 5V 电源供应/接入口 ◆ 1 组 3.3V 电源供应/接入口 ◆ 1 个启动模式选择配置接口 ◆ 1 个 2.4G 无线通信接口 ◆ 1 个 RTC 后备电池座,并带电池 ◆ 1 个复位按钮,可用于复位 MCU 和 LCD ◆ 3 个功能按钮,其中 WK_UP 兼具唤醒功能 ◆ 1 个电源开关,控制整个板的电源 ◆ 3.3V 与 5V 电源 TVS 保护,有效防止烧坏芯片。 ◆ 独创的一键下载功能 ◆ 除晶振占用的 IO 口外,其余所有 IO 口全部引出,其中 GPIOA 和 GPIOB 按顺序引 从上面的板载资源可以看出,MiniSTM32 开发板的板载资源是很丰富的,加上灵活的设计, 让您的开发变得更加简单。 ALIENTEK MiniSTM32 V3.0 开发板的特点包括: 1)小巧。整个板子尺寸为 8cm*10cm*2cm(包括液晶,但不计算铜柱的高度)。 2)灵活。板上除晶振外的所有的 IO 口全部引出,特别还有 GPIOA 和 GPIOB 的 IO 口是按 顺序引出的,可以极大的方便大家扩展及使用,另外板载独特的一键下载功能,避免了频 繁设置 B0、B1 带来的麻烦,直接在电脑上一键下载。 3) 资源丰富。板载十多种外设及接口,可以充分挖掘 STM32 的潜质。 4) 质量过硬。沉金 PCB+全新优质元器件+定制全铜镀金排针/排座+电源 TVS 保护,坚若磐 石。 5) 人性化设计。各个接口都有丝印标注,使用起来一目了然;接口位置设计安排合理,方 便顺手。资源搭配合理,物尽其用。 6 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 1.2 ALIENTEK MiniSTM32 开发板资源说明 资源说明部分,我们将分为两个部分说明:硬件资源说明和软件资源说明。 1.2.1 硬件资源说明 这里我们首先详细介绍 MiniSTM32 开发板的各个部分(图 1.1.1 中的标注部分)的硬件资 源,我们将按逆时针的顺序依次介绍。 1. HS0038 红外接收头 这是开发板板载的标准 38K 红外信号接收头,用于接收红外遥控器的信号,有了它,就可 以用红外遥控器控制这款开发板了,也可以用来做红外解码等其他相关实验。ALIENTEK MiniSTM32 开发板标配了一个红外遥控器,其外观如图 1.2.1.1 所示: 图 1.2.1.1 红外遥控器图片 关于该遥控器的使用,在第二十七章会有详细介绍。 2. DS18B20 预留接口 这是开发板预留的数字温度传感器 DS18B20/DS1820 接口,采用的是镀金的圆孔母座。当 要做 DS18B20 实验的时候,直接插到这个母座上即可,很方便。DS18B20 需自备,插上就可 以用的。同样 ALIENTEK 提供了 DS18B20 的相关例程。 3. USB 串口/串口 1 这是 USB 转串口(P4)同 STM32F103RCT6 的串口 1 进行连接的接口,标号 RXD 和 TXD 是 USB 转串口的 2 个数据口(对 CH340G 来说),而 PA9(TXD)和 PA10(RXD)则是 STM32 的串 口 1 的两个数据口(复用功能下)。他们通过跳线帽对接,就可以和连接在一起了,从而实现 STM32 的程序下载以及串口通信。 设计成 USB 串口,是出于现在电脑上串口正在消失,尤其是笔记本,几乎清一色的没有串 口。所以板载了 USB 串口可以方便大家下载代码和调试。而在板子上并没有直接连接在一起, 则是出于使用方便的考虑。这样设计,你可以把开发板当成一个 USB 转 TTL 串口来使用,从 而和其他板子进行通信,而其他板子的串口,也可以方便地接到我们的开发板上。 4. 两个 LED 灯 这是开发板板载的两个 LED 灯,它们在开发板上的标号为:DS0 和 DS1。DS0 是红色的, DS1 是绿色的,主要是方便大家识别。一般的应用 2 个 LED 足够了,在调试代码的时候,使用 LED 来指示程序状态,是非常不错的一个辅助调试方法。ALIENTEK 开发板几乎每个实例都使 7 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 用了 LED 来指示程序的运行状态。 5. STM32 USB 口 这是开发板板载的一个 MiniUSB 头,用于 STM32 与电脑的 USB 通讯(注意不是 USB 转 串口!!,一键下载的时候不是用这个 USB 口!!),此 MiniUSB 头在开发板上的标号为:USB, 用于连接 STM32F103RCT6 自带的 USB,通过此 MiniUSB 头,开发板就可以和电脑进行 USB 通信了。开发板总共板载了 2 个 MiniUSB 头,一个用于接 USB 串口,连接 CH340G 芯片;另 外一个用于 STM32 内带的 USB 连接。 开发板通过 MiniUSB 口供电,板载两个 MiniUSB 头(不共用),主要是考虑了使用的方便 性,以及可以给板子提供更大的电流(两个 USB 都接上)这两个因素。 6. 24C02 EEPROM 这是开发板板载的 2Kbit(256 个字节)EEPROM ,型号为:24C02,用于掉电数据保存。 因为 STM32 内部没有 EEPROM,所开发板外扩了 24C02,用于存储重要数据,也可以用来做 IIC 实验,及其他应用。该芯片直接挂在 STM32 的 IO 口上。 7. JTAG/SWD 这是开发板板载的 20 针标准 JTAG 调试口,在开发板上的标号为:JTAG。该 JTAG 口直 接可以和 ULINK 或者 JLINK 或者 STLINK 等调试器(仿真器)连接,同时由于 STM32 支持 SWD 调试,这个 JTAG 口也可以用 SWD 模式来连接。 用标准的 JTAG 调试,需要占用 5 个 IO 口,很多时候,可能造成 IO 口不够用,而用 SWD 则只需要 2 个 IO 口,大大节约了 IO 数量,但他们达到的效果是一样的。所以调试下载的时候, 强烈建议使用 SWD 模式!!! 8. CH340G 这是开发板板载的 USB 转串口芯片,型号为:CH340G。有了这个芯片,我们就可以实现 USB 转串口,从而能实现 USB 下载代码,串口通信等。 9. USB 转串口接口 这是开发板板载的另外一个 MiniUSB 头(USB_232),用于 USB 连接 CH340G 芯片,从而 实现 USB 转串口,所以串口下载代码的时候,USB 一定是要接在这个口上的。同时,此 MiniUSB 接头也是开发板电源的主要提供口。 10. STM32 启动配置选择 这是开发板板载的启动模式选择开关,在开发板上的标号为:BOOT。STM32 有 BOOT0 (B0)和 BOOT1(B1)两个启动选择引脚,用于选择复位后 STM32 的启动模式,默认 B0, B1 都是连接在 GND 的。作为开发板,这两个是必须的。在开发板上,我们通过跳线帽选择 STM32 的启动模式。关于启动模式的说明,请看 2.1.1 节。 11. 电源指示灯 这是开发板板载的一颗蓝色的 LED,用于指示电源状态,在开发板上的标号为:PWR。在 电源开启的时候(通过板上的电源开关控制),该灯会亮,否则不亮。通过这个 LED,可以判 断开发板的上电情况,开发板必须在上电的条件下(电源灯亮),才可以正常使用。 12. 复位按键 这是开发板板载的复位按键,用于复位 STM32,同时还具有复位液晶的功能,因为液晶模 块的复位引脚和 STM32 的复位引脚是连接在一起的,此按键在开发板上的标号为:RESET。 当按下该键的时候,STM32 和液晶一并被复位。 13. WK_UP 按键 这是开发板板载的一个唤醒按键,该按键连接到 STM32 的 WAKE_UP(PA0)引脚,可用 于待机模式下的唤醒,在不使用唤醒功能的时候,也可以做为普通按键输入使用,此按键在开 8 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 发板上的标号为:WK_UP。 14. 两个普通按键 这是开发板板载的两个普通按键,可以用于人机交互的输入,这两个按键是直接连接在 STM32 的 IO 口上的,两个按键在开发板上的标号分别为:KEY0、KEY1。 15. 电源芯片 这是开发板的电源稳压芯片,型号为:AMS1117-3.3。因为 STM32 是 3.3V 供电的,所以 我们需要将 USB 的 5V 电压转换为 3.3V,这个芯片就是将 5V 转换为 3.3V 的线性稳压芯片。 16. 电源开关 这是开发板板载的电源开关,此开关在开发板上的标号为:K1,并标有 ON/OFF 丝印。该 开关用于控制整个开发板的供电,如果切断,则整个开发板都将断电,电源指示灯(PWR)会 随着此开关的状态而亮灭。 17. 2 个 LED 灯 这是开发板板载的一个标准 PS/2 接头,用于连接电脑鼠标和键盘等 PS/2 设备,在开发板 上的标号为:PS/2。通过该接口,我们仅需要 2 个 IO 口,就可以扩展一个键盘,所以大家不必 要对板上只有 3 个按键而感到担忧。ALIENTEK 提供了标准的鼠标驱动例程,方便大家学习 PS/2 协议。 18. 3.3V 电源输出/输入 这是开发板板载的一组 3.3V 电源输入输出排针(2*3),在开发板上的标号为:VOUT1。 该排针用于给外部提供 3.3V 的电源,也可以用于从外部取 3.3V 的电源给板子供电。大家在实 验的时候可能经常会为没有 3.3V 电源而苦恼不已,ALIENTEK 充分考虑到了大家需求,有了 这组 3.3V 排针,您就可以很方便的拥有一个简单的 3.3V 电源(最大电流不能超过 500ma),另 外板载了 3.3V TVS 管,能有效吸收高压脉冲,防止外接设备/电源可能对开发板造成的损坏。 19. 5V 电源输出/输入 这是开发板板载的一组 5V 电源输入输出排针(2*3),在开发板上的标号为:VOUT2,用 于给外部提供 5V 的电源,也可以用于从外部取 5V 的电源给板子供电。同样大家在实验的时候 可能经常会为没有 5V 电源而苦恼不已,有了 ALIENTEK MiniSTM32 开发板,您就可以很方便 的拥有一个简单的 5V 电源(最大电流不能超过 500ma),另外板载了 5V TVS 管,能有效吸收 高压脉冲,防止外接设备/电源可能对开发板造成的损坏。 20. GPIOC&D 引出 IO 口 这是开发板板载的 GPIOC 与 GPIOD 等 IO 口的引出排针,在开发板上的标号为:P5。我 们可以用这些引出的 IO 口来连接外部模块,方便大家外接其他模块。 21. SD 卡接口 这是开发板板载的 SD 卡接口。SD 卡作最常见的存储设备之一,是很多数码设备的存储媒 介,比如数码相框、数码相机、MP5、手机、平板电脑等。我们的开发板自带了 SD 卡接口(大 卡),可以用于 SD 卡实验,方便大家学习 SD 卡,TF 卡通过转接座也可以很方便的接到我们的 开发板上。 有了它,开发板就相当于拥有了一个大容量的外部存储器,不但可以用来提供数据,也可 以用来存储数据,使得这款开发板可以完成更多的功能。 这里要特别说明一下:该 SD 卡卡座是在开发板的背面! 22. W25Q64 64M FLASH 这是开发板板载的一颗 FLASH 芯片,型号为 W25Q64。这颗芯片的容量为 64M bit,也就 是 8M 字节。有了这颗芯片,我们就可以存储一些不常修改的数据到里面,比如字库等,从而 大大节省对 STM32 内部 FLASH 的占用。关于该芯片的使用见 SPI 实验这个章节。 9 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 23. NRF24L01 模块接口 这是开发板板载的 NRF24L01 模块接口,只要插入 NRF24L01 无线模块,我们便可以实现 无线通信功能。但是提醒大家:NRF24L01 通信,至少需要 2 个模块和 2 个开发板同时工作才 可以。如果只有 1 个开发板或 1 个模块,是没法实现无线通信的。 24. GPIOB&C 引出 IO 口 这是开发板板载的 GPIOB 与 GPIOC 的引出口,该接口用于将 STM32 的 GPIOB 和部分的 GPIOC 引出,方便大家的使用,在开发板上的标号为:P1。这里 GPIOB 全部使用顺序引出的 方式,尤其适合外部总线型器件的接入。 25. STM32F103RCT6 这是开发板的核心芯片,从 3.0 版本开始,升级到 RCT6,详细型号为:STM32F103RCT6。 该芯片具有 48K SRAM、256K FLASH、2 个 16 位基本定时器、4 个 16 位通用定时器、2 个 16 位高级定时器、2 个 DMA 控制器、3 个 SPI、2 个 IIC、5 个串口、1 个 USB、1 个 CAN、3 个 12 位 ADC、1 个 12 位 DAC、1 个 SDIO 接口、51 个通用 IO 口。 26. OLED&LCD 共用接口 这是 ALIENTEK 开发板的特色设计,一个接口,兼容两种模块。在此部分,LCD 的部分 IO 和 OLED 的 IO 共用,具体请参看后面的开发板原理图。这样我们一个接口既可以接 LCD 模块,又可以接 OLED 模块。OLED 模块使用的是 ALIENTEK 的 OLED 模块,分辨率为 128*64, 模块大小为 2.6cm*2.7cm。而 LCD 模块,则可以使用 ALIENTEK 全系列的 TFTLCD 模块,包 括:2.4 寸(电阻屏,240*320)、2.8 寸(电阻屏,240*320)、3.5 寸(电阻屏,320*480)、4.3 寸(电容屏,800*480)、7 寸(电容屏,800*480)。 这里特别提醒:在使用的时候,OLED 模块是靠左插的,而 LCD 模块,则是靠右插,在后 续章节我们将分别介绍 OLED 模块和 LCD 模块的使用。 27. GPIOA 引出 IO 口 这是开发板 GPIOA 的引出排针,在开发板上的标号为 P3。ALIENTEK 开发板将所有的 IO 口(除了 2 个晶振占用的 4 个 IO 口)都用排针引出来了,而且 GPIOA 和 GPIOB 是按顺序引 出的。按顺序引出,在很多时候能方便大家的实验和测试,比如外接带并行控制的器件,有了 并行引出的排针,那么就可以很方便的通过这些排针连接到外部设备了。 将开发板的 IO 口全部排针引出,大家就可以用来外接其他模块等,不论调试还是功能扩 展都是很方便的。 28. 红外&温度传感器连接口 这是开发板板载的红外与温度传感器的连接接口,开发板虽然自带了红外接收头和 DS18B20 的接口,但是并没有将这两个器件直接挂在 IO 口上,而是通过跳线帽来连接,以防 止在不使用这两器件的时候,他们对 IO 口的干扰,当然我们也可以用跳线,把 DS18B20 和红 外遥控接收模块接到其他电路上使用。 1.2.2 软件资源说明 上面我们详细介绍了 ALIENTEK MiniSTM32 开发板的硬件资源。接下来,我们将向大家 简要介绍一下开发板的软件资源。 MiniSTM32 开发板提供的标准例程多达 39 个,一般的 STM32 开发板仅提供库函数代码, 而我们则提供寄存器和库函数两个版本的代码(本手册以寄存器版本例程作为介绍)。我们提供 的这些例程,基本都是原创,拥有非常详细的注释,代码风格统一、循序渐进,非常适合初学 者入门。而其他开发板的例程,大都是来自 ST 库函数的直接修改,注释也比较少,对初学者 来说不那么容易入门。 10 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 MiniSTM32 开发板的例程列表如表 1.2.2.1 所示: 编号 实验名字 编号 实验名字 1 跑马灯实验 21 触摸屏实验(支持电容/电阻屏) 2 按键输入实验 22 红外遥控实验 3 串口实验 23 DS18B20 数字温度传感器实验 4 外部中断实验 24 无线通信实验 5 独立看门狗实验 25 PS2 鼠标实验 6 窗口看门狗实验 26 FLASH 模拟 EEPROM 实验 7 定时器中断实验 27 内存管理实验 8 PWM 输出实验 28 SD 卡实验 9 输入捕获实验 29 FATFS 实验 10 OLED 实验 30 汉字显示实验(支持 12/16/24 字体大小) 11 TFTLCD 实验 31 图片显示实验 12 USMART 调试组件实验 32 串口 IAP 实验 13 RTC 实验 33 触控 USB 鼠标实验 14 待机唤醒实验 34 USB 读卡器实验 15 ADC 实验 35 UCOSII 实验 1-任务调度 16 内部温度传感器实验 36 UCOSII 实验 2-信号量和邮箱 17 DAC 实验 37 UCOSII 实验 3-消息队列、信号量集和软件定时器 18 DMA 实验 38 综合实验 19 IIC 实验 39 ucGUI 实验 20 SPI 实验 表 1.2.2.1 ALIENTEK MiniSTM32 开发板例程表 从上表可以看出,ALIENTEK MiniSTM32 开发板的例程基本上涵盖了 STM32F103RCT6 的所有内部资源,并且外扩展了很多有价值的例程,比如:FLASH 模拟 EEPROM 实验、内存 管理实验、FATFS 实验、IAP 实验、综合实验等。 而且从上表可以看出,例程安排是循序渐进的,首先从最基础的跑马灯开始,然后一步步 深入,从简单到复杂,有利于大家的学习和掌握。所以,ALIENTEK MiniSTM32 开发板是非常 适合初学者的。当然,对于想深入了解 STM32 内部资源的朋友,ALIENTEK MiniSTM32 开发 板也绝对是一个不错的选择。 这里特别说明一下综合实验,这个实验使得 ALIENTEK MiniSTM32 开发板更像一个产品, 而不单单是一个开发板了,它采用 ALIENTEK 自己编写的 GUI 系统,自动兼容各种分辨率 (320*240/480*320/800*480)的屏幕,支持电阻和电容触摸屏,可玩性高。该实验集成了文件 系统(读&写)、图片显示、T9 拼音输入法、手写识别、多国语言切换、记事本和 USB 连接等 高级功能,具有极高的参考价值。 11 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 1.3 ALIENTEK MiniSTM32 V3.0 开发板升级说明 ALIENTEK MiniSTM32 V3.0 开发板相对于过往版本,主要变化如表 1.3.1 所示: 编号 对比项 ALIENTEK MiniSTM32 开发板 之前版本 V3.0 版本 说明 1 CPU STM32F103RBT6 STM32F103RCT6 资源更多 2 USB 转串口芯片 PL2303HX CH340G 更稳定 3 SPI FLASH 芯片 W25Q16 W25Q64 容量更大 4 JF24C/D 接口 预留 去掉 去掉不常用的接口 5 PA1 JF24_FIFO NRF_IRQ 引脚变更 6 PC5 NRF_IRQ KEY0/PS_DAT 引脚变更 7 PA13 KEY0/PS_DAT JTMS/SWDIO 引脚变更 表 1.3.1 V3.0 版本 VS 过往版本硬件变更表 从表 1.3.1 可以看出,前面四项,是硬件升级,后面 3 项是线路变更。 硬件升级方面:CPU 采用更多资源的 STM32F103RCT6,相比 RBT6,资源多了不少,集成度 更高,功能更强。USB 转串口芯片改为采用与战舰 STM32 开发板相同的 CH340G,更稳定,不容 易出现兼容性问题。SPI FLASH 芯片同样改为采用与战舰 STM32 开发板相同的 W25Q64,容量是 W25Q16 的 4 倍,可以存储更多内容。另外,V3.0 版本去掉了不常用的 JF24C/D 模块接口。 线路变更方面,做了三项改变:PA1 原来是连接 JF24_FIFO 信号的,V3.0 改为连接 NRF_IRQ 信号。PC5 原来是用来连接 NRF_IRQ 信号,V3.0 改为连接 KEY0/PS_DAT 信号。而 PA13 原来是连 接 KEY0/PS_DAT 信号的,V3.0 改为不连接任何外设(仅作 JTMS/SWDIO 信号)。经过这样的变更 以后,PA13(SWDIO)空出来了,所以 V3.0 开发板便可以支持所有例程 SWD 在线仿真了,原来 的版本存在有按键的例程,就不能仿真这样的缺陷,在 V3.0 上面,这个缺陷得到了圆满解决。 12 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二章 实验平台硬件资源详解 本章,我们将节将向大家详细介绍 ALIENTEK MiniSTM32 开发板各部分的硬件原理图, 让大家对该开发板的各部分硬件原理有个深入理解,并向大家介绍开发板的使用注意事项,为 后面的学习做好准备。 本章将分为如下两节: 2.1,开发板原理图详解; 2.2,开发板使用注意事项; 2.3,STM32 学习方法; 2.1 开发板原理图详解 2.1.1 MCU ALIENTEK MiniSTM32 V3.0 版开发板选择的是 STM32F103RCT6 作为 MCU,它拥有的资 源包括:48KB SRAM、256KB FLASH、2 个基本定时器、4 个通用定时器、2 个高级定时器、 2 个 DMA 控制器(共 12 个通道)、3 个 SPI、2 个 IIC、5 个串口、1 个 USB、1 个 CAN、3 个 12 位 ADC、1 个 12 位 DAC、1 个 SDIO 接口及 51 个通用 IO 口。该芯片性价比极高,MCU 部 分的原理图如图 2.1.1.1(因为原理图比较大,缩小下来可能有点看不清,请大家打开开发板光 盘的原理图进行查看)所示: 图 2.1.1.1 MCU 部分原理图 上图中中上部的 BOOT1 用于设置 STM32 的启动方式,其对应启动模式如下表所示: 表 2.1.1.1 BOOT0、BOOT1 启动模式表 按照表 2.1.1.1,一般情况下(即标准的 ISP 下载步骤)如果我们想用用串口下载代码,则 必须先配置 BOOT0 为 1,BOOT1 为 0,然后按复位键,最后再通过程序下载代码,下载完以 13 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 后又需要将 BOOT0 设置为 GND,以便每次复位后都可以运行用户代码。可以看到,这个标准 的 ISP 步骤还是很繁琐的,跳线帽跳来跳去,还要手动复位,所以 ALIENTEK 为 STM32 的串 口下载专门设计了一键下载电路,通过串口的 DTR 和 RTS 信号,来自动控制 RST(复位)和 BOOT0,因此不需要用户来手动切换状态,直接串口下载软件自动控制,可以非常方便的下载 代码,这是其他开发板所不具备的。 P3 和 P1 分别用于 PORTA 和 PORTB 的 IO 口引出,其中 P1 有部分用于 PORTC 口的引出。 PORTA 和 PORTB 都是按顺序排列的,这样设计的目的是为了让大家更方便地与外部设备连接。 P2 连接了 DS18B20 的数据口以及红外传感器的数据线,它们分别对应着 PA0 和 PA1,只 需要通过跳线帽将 P2 和 P3 连接起来就可以使用了。这里不直接连在一起的原因有二:1,防 止红外传感器和 DS18B20 对这两个 IO 口作为其他功能使用的时候的影响;2,DS18B20 和红 外传感器还可以用来给其他板子提供输入,等于我们的板子为别的板子提供了红外接口和温度 传感器,在调试的时候,还是蛮有用的。 P4 口连接了 CH340G 的串口输出,对应着 STM32 的串口 1(PA9/PA10),在使用的时候, 也是通过跳线帽将这两处连接起来。这样设计有两个好处:1,使得 PA9 和 PA10 用作其他用途 使用的时候(比如串口 1 连接其他串口设备),不受到 CH340G 的影响。2,USB 转串口可以用 作他用,并不仅限这个板上的 STM32 使用,也可以连接到其他板子上,这样 ALIENEK MiniSTM32 开发板就相当于一个 USB 转 TTL 串口。 P5 口是另外一组 IO 引出排针,将 PORTC 和 PORTD 等的剩余 IO 口从这里引出。在此部 分原理图中,我们还可以看到 STM32F103RCT6 的各个 IO 口与外设的连接关系,这些将在后 面给大家介绍。 这里 STM32 的 VBAT 采用 CR1220 纽扣电池和 VCC3.3 混合供电的方式,在有外部电源 (VCC3.3)的时候,CR1220 不给 VBAT 供电,而在外部电源断开的时候,则由 CR1220 给 VBAT 供电。这样,VBAT 总是有电的,以保证 RTC 的走时以及后备寄存器的内容不丢失。 该部分还有 JTAG,JTAG 部分电路如下图: 图 2.1.1.2 JTAG 原理图 这里采用的是标准的 JTAG 接法,但是 STM32 还有 SWD 接口,SWD 只需要最少 2 跟线 (SWCLK 和 SWDIO)就可以下载并调试代码了,这同我们使用串口下载代码差不多,而且速 度更快,能调试。所以建议大家在设计产品的时候,可以留出 SWD 来下载调试代码,而摒弃 JTAG。STM32 的 SWD 接口与 JTAG 是共用的,只要接上 JTAG,你就可以使用 SWD 模式了 (其实 SWD 并不需要 JTAG 这么多线),JLINK V8/JLINK V7/ULINK2 以及 ST LINK 等都支持 SWD。这里,我们推荐使用 SWD 模式,不推荐 JTAG 模式。 14 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 2.1.2 EEPROM ALIENTEK MiniSTM32 开发板自带了 24C02 这颗 EEPROM 芯片,该芯片的容量为 2Kbit, 也就是 256 个字节,对于我们普通应用来说是足够了的。你也可以选择换大的芯片,因为在原 理上是兼容 24C02~24C512 全系列的 EEPROM 芯片的。其原理图如下: 图 2.1.2.1 EEPROM 原理图 这里我们把 A0~A2 均接地,对 24C02 来说也就是把地址位设置成了 0 了,写程序的时候 要注意这点。IIC_SCL 接在 MCU 的 PC12 上,IIC_SDA 接在 MCU 的 PC11 上,这里我们并没 有接到 STM32 内部的 IIC 上,因为 STM32 的硬件 IIC 十分不好用,而且不稳定!如果你想在 开发板上使用硬件 IIC,那么也是可以的,你只需要设置 PC11 和 PC12 为浮空输入,然后把 PB10 和 PB11(IIC2)或者 PB6 和 PB7(IIC1)通过飞线连接到 PC11 和 PC12 上就可以使用硬件 IIC 了。 2.1.3 温度传感器 温度传感器我们使用的是 DS18B20,其原理图如下: 图 2.1.3.1 温度传感器原理图 DS18B20 的数据脚(18B20_DQ)接 P2 的第一脚,并没有直接连接到 MCU,至于为什么, 前面已有介绍。要使用 18B20 的时候,我们用跳线帽把 PA0 和 P2-1 连接起来就可以了。 2.1.4 按键 ALIENTEK MiniSTM32 开发板总共有 3 个按键,其原理图如下: 15 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 2.1.4.1 按键输入原理图 KEY0 和 KEY1 用作普通按键输入,分别连接在 PC5 和 PA15 上,其中 PA15 和 JTDI 共用 了,所以,在使用 KEY0 和 KEY1 的时候,就不能使用 JTAG 来调试了,但是可以用 SWD 调 试,这点在使用的时候要注意。KEY0 和 KEY1 还和 PS/2 的 DAT 和 CLK 线共用。 WK_UP 按键连接到 PA0(STM32 的 WKUP 引脚),它除了可以用作普通输入按键外,还可 以用作 STM32 的唤醒输入。该按键是高电平触发的。由于 PA0 还是 DS18B20 的输入引脚,而 18B20 是有上拉电阻的,所以在使用 WK_UP 按键的时候,请一定要断开 PA0 和 DS18B20 的 跳线帽。 2.1.5 液晶显示模块 ALIENTEK MiniSTM32 开发板载有目前比较通用的液晶显示模块接口,还有其比较有特色 的兼容性接口,不仅支持 ALIENTEK 各种尺寸(2.4、2.8、3.5、4.3、7 寸等)的 TFTLCD,还 支持 OLED 显示器。同时,该接口支持电阻触摸屏以及电容触摸屏等不同类型的触摸屏接口, 其原理图如下: 图 2.1.5.1 液晶显示模块原理图 TFT_LCD 是一个通用的液晶模块接口。OLED 是一个给 OLED 显示模块供电的接口,它 和 TFT_LCD 拼接在一起。当使用 TFTLCD 时,我们接到 TFT_LCD 上(靠右插)就可以了, 而当我们使用 ALIENTEK 的 OLED 模块时,则接 OLED 排针做电源,同时会连接到 TFT_LCD 上(靠左插)的部分管脚,从而实现 OLED 与 MCU 的连接。ALIENTEK MiniSTM32 的 LCD 接口兼容 ALIENTEK 各种尺寸的 TFTLCD 模块,包括:2.4 寸(320*240,电阻屏)、2.8 寸(320*240, 电阻屏)、3.5 寸(480*320,电阻屏)、4.3 寸(800*480,电容屏)、7 寸(800*480,电容屏) 等,同时还兼容 ALIENTEK 的 0.96 寸 OLED 模块。 这些引脚与 MCU 的连接关系我们在这里就不一一列出了,大家可以从 MCU 的原理图上 找到。 2.1.6 红外接收头 ALIENTEK MiniSTM32 开发板载有红外接收传感器 HS0038,原理图如下: 16 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 2.1.6.1 红外接收头原理图 REMOTE_IN 接到 P2 的第二脚,也没有直接接在 MCU 的 IO 口上,目的也是防止 IO 口在 做其他功能使用的时候,收到红外信号的干扰。 2.1.7 PS/2 接口 ALIENTEK MiniSTM32 开发板载有 PS/2 接口,有了该接口,我们就可以用来连接外部标 准的 PS/2 鼠标键盘了,也就大大的扩展了开发板的输入。原理图如下: 图 2.1.7.1 PS/2 接口原理图 PS_CLK 和 PS_DAT 分别接 PA15 和 PC5,PS/2 的信号线是需要外部提供上拉电阻的,这 里 PS_CLK 与 JTCK 共用一个上拉电阻,而 PS_DAT 则需要使用 STM32 内部的上拉电阻了, 在使用的时候,需要注意,记得开启 PC5 的上拉电阻。 2.1.8 LED ALIENTEK MiniSTM32 开发板上总共有 3 个 LED,其原理图如下: 图 2.1.8.1 LED 原理图 其中 PWR 是开发板电源指示灯,为蓝色。LED0 和 LED1 分别接在 PA8 和 PD2 上,PA8 17 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 还可以通过 TIM1 的通道 1 的 PWM 输出来控制 DS0 的亮度。为了方便大家判断,我们选择了 DS0 为红色,DS1 为绿色的 LED 灯。 2.1.9 SD 卡 ALIENTEK MiniSTM32 开发板载有标准的 SD 卡接口(在开发板背面),有了这个接口, 我们就可以外扩大容量存储设备,可以用来记录数据。其原理图如下: 图 2.1.9.1 SD 卡接口原理图 SD 卡我们使用的是 SPI 模式通信,SD 卡的 SPI 接口连接到 STM32 的 SPI1 上,SD_CS 接 在 PA3 上,开发板上的 SPI1 总共由 3 个外设共用,他们分别是:SD 卡、NRF24L01 无线模块、 和 W25Q64,可以通过不同的片选信号来分时复用。 2.1.10 无线模块 ALIENTEK MiniSTM32 开发板板载了 NRF24L01 无线模块的接口。该接口用来连接 NRF24L01 等 2.4G 无线模块,从而实现开发板与其他设备的无线数据传输(注意:NRF24L01 不能和蓝牙/WIFI 连接)。NRF24L01 无线模块的最大传输速度可以达到 2Mbps,传输距离最大 可以到 30 米左右(空旷地,无干扰)。有了这个接口,我们就可以做无线通信,以及其他很多 的相关应用了。这部分原理图如下: 图 2.1.10.1 无线模块接口原理图 NRF_CE/NRF_CS/NRF_IRQ 连接在 STM32F103RCT6 的 PA4/PC4/PA1 上,而另外 3 个 SPI 18 信号则和 SPI FLASH 共用。 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 2.1.11 SPI FLASH ALIENTEK MiniSTM32 开发板载有 SPI FLASH 芯片 W25Q64,该芯片的容量为 8M 字节, 其原理图如下: 图 2.1.11.1 W25Q64 原理图 W25Q64 也是共用了 SPI1,F_CS 接在 PA2 上。至此,总共 SPI1 的三个器件都已介绍完毕, 他们的 CS 都接在不同的 IO 口上,所以在使用其中一个器件的时候,要记得禁止其他器件的 CS 脚,否则会有干扰。 2.1.12 USB 串口、USB、电源 这里三个部分一起介绍,ALIENTEK MiniSTM32 开发板板载了 USB 串口,并且由 USB 提 供电源,使得我们只需要一根 USB 线就可以使用 ALIENTEK MiniSTM32 开发板了,包括串口 下载代码、供电、串口通信 3 位一体。 开发板的供电部分还引出了 5V(VOUT2)和 3.3V(VOUT1)的排针,可以用来为外部设 备提供电源或者从外部引入电源,这在很多时候是非常有用的,有时候你突然要一个 3.3V 的电 源,但找半天就是没这样的电源,而我们的板子则可直接向外部提供 3.3V 电源,有了它,你就 可以给外部设备提供 3.3V、5V 电源了。注意电流不能太大哦! 开发板的 USB 接口(USB)通过独立的 Mini USB 头引出,不和 USB 转串口(USB_232) 共用,这样不但可以同时使用,还可以给系统提供更大的电流。 这几个部分的原理图如下: 19 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 2.1.12.1 USB 串口、USB、电源部分原理图 图中的 Q1 和 Q2 外加几个电阻和一个二极管就构成了开发板的一键下载电路,此电路通过 RST 和 DTR 信号来控制 BOOT0 和 RESET 信号,从而实现一键下载的功能。很多朋友问一键 下载的原理,这里和大家讲一下,先说一个前提:DTR_N 和 RTS_N 的输出和 DTR/RTS 的设 置是相反的。必须先记下这个前提。 一键下载电路的具体实现过程:首先,mcuisp 控制 DTR 输出低电平,则 DTR_N 输出高, 然后 RTS 置高,则 RTS_N 输出低,这样 Q2 导通了,BOOT0 被拉高,即实现设置 BOOT0 为 1, 同时 Q1 也会导通,STM32 的复位脚被拉低,实现复位。然后,延时 100ms 后,mcuisp 控制 DTR 为高电平,则 DTR_N 输出低电平,RTS 维持高电平,则 RTS_N 继续为低电平,此时 STM32 的复位引脚,由于 Q1 不再导通,变为高电平,STM32 结束复位,但是 BOOT0 还是维持为 1, 从而进入 ISP 模式,接着 mcuisp 就可以开始连接 STM32,下载代码了,从而实现一键下载。 另外,此部分还有一个开关 K1,用来控制整个系统的供电,如果断开则整个系统的 3.3V 部分都将断电。而 5V 部分的电源还是开启的。图中 F1 为可恢复保险丝,用于保护 USB。 图中的 D4 和 D5 这两个 TVS 管,用于保护开发板,防止外部高压脉冲/静电损坏开发板上 的元器件,让大家用的更加放心。 2.2 开发板使用注意事项 为了让大家更好的使用 ALIENTEK MiniSTM32 开发板,我们在这里总结该开发板使用的 时候尤其要注意的一些问题,希望大家在使用的时候多多注意,以减少不必要的问题。 1, 开发板一般情况是由 USB_232 口供电,在第一次上电的时候由于 CH340G 在和电脑建 立连接的过程中,导致 DTR/RTS 信号不稳定,会引起 STM32 复位 2~5 次左右,这个 20 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 现象是正常的,后续按复位键就不会出现这种问题了。 2, 虽说开发板有 500mA 自恢复保险丝,但是由于自恢复保险丝是慢动作器件,所以在给 外部供电的时候,还是请大家小心一点,不要超过这个限额,以免引起不必要的问题 3, SPI1 被多个 SPI 器件共用(SD 卡/无线模块/W25Q64),在使用的时候,必须保证同一 时刻只有 1 个 SPI 器件是被选中的(CS 为低),其他器件必须设置为非选中(CS 为高), 以免互相干扰。 4, JTAG 接口有几个信号(JTDO/JTRST/JTDI 等)和 LCD/KEY1 等共用了,所以在使用 的时候注意,一旦用到这些有冲突的引脚,就不能再用 JTAG 模式仿真/下载代码了, 必须使用 SWD 模式,所以我们极力推荐使用:SWD 模式。 5, 当你想使用某个 IO 口用作其他用处的时候,请先看看开发板的原理图,该 IO 口是否 有连接在开发板的某个外设上,如果有,该外设的这个信号是否会对你的使用造成干 扰,先确定无干扰,再使用这个 IO。比如 PA0 如果和 1820 的跳线帽连接上了,那么 WK_UP 按键就无法正常检测了,按键实验,也就没法做了。 6, 当液晶显示白屏的时候,请先检查液晶模块是否插好(拔下来重新插试试),如还不行, 可以通过串口看看 LCD ID(按一次复位,输出一次)是否正常,再做进一步分析。 7, 当使用液晶模块(16 位模式)的时候,PB0~PB15 都被占用了,可以分时复用,但是 在写程序的时候要注意,这里还有连接到触摸屏的 PC0/PC1/PC2/PC3/PC13 均会存在这 样的问题,在使用的时候要格外注意,看是否会产生干扰。 至此,本手册的实验平台(ALIENTEK MiniSTM32 开发板)的硬件部分就介绍完了,了解 了整个硬件对我们后面的学习会有很大帮助,有助于理解后面的代码,在编写软件的时候,可 以事半功倍,希望大家细读!另外 ALIENTEK 开发板的其他资料及教程更新,都可以在技术论 坛 www.openedv.com 下载到,大家可以经常去这个论坛获取更新的信息。 2.3 STM32 学习方法 STM32 作为目前最热门的 ARM Cortex M3 处理器,正在被越来越多的公司选择使用。学 习 STM32 的朋友也越来越多,初学者,可能会认为 STM32 很难学,以前只学过 51,或者甚至 连 51 都没学过的,一看到 STM32 那么多寄存器,就懵了。其实,万事开头难,只要掌握了方 法,学好 STM32,还是非常简单的,这里我们总结学习 STM32 的几个要点: 1,一款实用的开发板。 这个是实验的基础,有时候软件仿真通过了,在板上并不一定能跑起来,而且有个开发板 在手,什么东西都可以直观的看到,效果不是仿真能比的。但开发板不宜多,多了的话连自己 都不知道该学哪个了,觉得这个也还可以,那个也不错,那就这个学半天,那个学半天,结果 学个四不像。倒不如从一而终,学完一个在学另外一个。 2,两本参考资料,即《STM32 参考手册》和《Cortex-M3 权威指南》。 《STM32 参考手册》是 ST 出的官方资料,有 STM32 的详细介绍,包括了 STM32 的各种 寄存器定义以及功能等,是学习 STM32 的必备资料之一。而《Cortex-M3 权威指南》则是对 《STM32 参考手册》的补充,后者一般认为使用 STM32 的人都对 CM3 有了较深的了解,所以 Cortex-M3 的很多东西它只是一笔带过,但前者对 Cortex-M3 有非常详细的说明,这样两者搭 配,你就基本上任何问题都能得到解决了。 3,掌握方法,勤学慎思。 STM32 不是妖魔鬼怪,不要畏难,STM32 的学习和普通单片机一样,基本方法就是: a) 掌握时钟树图(见《STM32 中文参考手册_V10 版》图 8)。 21 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 任何单片机,必定是靠时钟驱动的,时钟就是单片机的动力,STM32 也不例外,通过时钟 树,我们可以知道,各种外设的时钟是怎么来的?有什么限制?从而理清思路,方便理解。 b) 多思考,多动手。 所谓熟能生巧,先要熟,才能巧。如何熟悉?这就要靠大家自己动手,多多练习了,光看/ 说,是没什么太多用的,很多人问我,STM32 这么多寄存器,如何记得啊?回答是:不需要全 部记住。我至今也就只记得 STM32 的 IO 口控制这几个寄存器,因为有规律可循,好记。其他 的一概不记得。学习 STM32,不是应试教育,不需要考试,不需要你倒背如流。你只需要知道 这些寄存器,在哪个地方,用到的时候,可以迅速查找到,就可以了。完全是可以翻书,可以 查资料的,可以抄袭的,不需要死记硬背。掌握学习的方法,远比掌握学习的内容重要的多。 熟悉了之后,就应该进一步思考,也就是所谓的巧了。我们提供了几十个例程,供大家学 习,跟着例程走,无非就是熟悉 STM32 的过程,只有进一步思考,才能更好的掌握 STM32, 也即所谓的举一反三。例程是死的,人是活的,所以,可以在例程的基础上,自由发挥,实现 更多的其他功能,并总结规律,为以后的学习/使用打下坚实的基础,如此,方能信手拈来。 所以,学习一定要自己动手,光看视频,光看文档,是不行的。举个简单的例子,你看视 频,教你如何煮饭,几分钟估计你就觉得学会了。实际上你可以自己测试下,是否真能煮好? 机会总是留给有准备的人,只有平时多做准备,才可能抓住机会。 只要以上三点做好了,学习 STM32 基本上就不会有什么太大问题了。如果遇到问题,可 以在我们的技术论坛:开源电子网:www.openedv.com 提问,论坛 STM32 板块已经有 2.4W 多 个主题,很多疑问已经有网友提过了,所以可以在论坛先搜索一下,很多时候,就可以直接找 到答案了。论坛是一个分享交流的好地方,是一个可以让大家互相学习,互相提高的平台,所 以有时间,可以多上去看看。 另外,很多 ST 官方发布的所有资料(芯片文档、用户手册、应用笔记、固件库、勘误手 册等),大家都可以在 www.stmcu.org 这个地方下载到。也可以经常关注下,ST 会将最新的资 料都放到这个网址。 22 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二篇 软件篇 上一篇,我们介绍了本手册的实验平台,本篇我们将详细介绍 STM32 的开发软件:MDK5。 通过该篇的学习,你将了解到:1、如何在 MDK5 下新建 STM32 工程;2、工程的编译;3、 MDK5 的一些使用技巧;4、软件仿真;5、程序下载;6、在线调试;以上几个环节概括了了 一个完整的 STM32 开发流程。本篇将图文并茂的向大家介绍以上几个方面,通过本篇的学习, 希望大家能掌握 STM32 的开发流程,并能独立开始 STM32 的编程和学习。 本篇将分为如下 3 个章节: 2.1,MDK5 软件入门; 2.2,STM32 开发基础知识入门 2.3,SYSTEM 文件介绍; 23 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三章 MDK5 软件入门 本章将向大家介绍 MDK5 软件的使用,通过本章的学习,我们最终将建立一个自己的 MDK5 工程,同时本章还将向大家介绍 MDK5 软件的一些使用技巧,希望大家在本章之后,能 够对 MDK5 这个软件有个比较全面的了解。 本章分为如下个小结: 3.1,STM32 官方固件库简介 3.2,MDK5 简介; 3.3,新建基于 V3.5 版本固件库的 MDK5 工程; 3.4,程序下载与调试 3.5,MDK5 使用技巧; 3.1 STM32 官方固件库简介 ST(意法半导体)为了方便用户开发程序,提供了一套丰富的 STM32 固件库。到底什么是固 件库?它与直接操作寄存器开发有什么区别和联系?很多初学用户很是费解,这一节,我们将 讲解 STM32 固件库相关的基础知识,希望能够让大家对 STM32 固件库有一个初步的了解,至 于固件库的详细使用方法,我们会在后面的章节一一介绍。这章节有一些图片是截图的权威手 册。这一节的知识可以参考《STM32 固件库使用手册中文翻译版》P32,固件库手册讲解更加 详细,这里只是提到一下,希望大家谅解。 固件库 V3.5 光盘路径(是压缩包形式,大家解压即可): 8,STM32 参考资料\STM32 固件库使用参考资料\ STM32F10x_StdPeriph_Lib_V3.5.0 3.1.1 库开发与寄存器开发的关系 很多用户都是从学 51 单片机开发转而想进一步学习 STM32 开发,他们习惯了 51 单片机 的寄存器开发方式,突然一个 ST 官方库摆在面前会一头雾水,不知道从何下手。下面我们将 通过一个简单的例子来告诉 STM32 固件库到底是什么,和寄存器开发有什么关系?其实一句 话就可以概括:固件库就是函数的集合,固件库函数的作用是向下负责与寄存器直接打交道, 向上提供用户函数调用的接口(API)。 在 51 的开发中我们常常的作法是直接操作寄存器,比如要控制某些 IO 口的状态,我们直 接操作寄存器: P0=0x11; 而在 STM32 的开发中,我们同样可以操作寄存器: GPIOx->BRR = 0x0011; 这种方法当然可以,但是这种方法的劣势是你需要去掌握每个寄存器的用法,你才能正确使用 STM32,而对于 STM32 这种级别的 MCU,数百个寄存器记起来又是谈何容易。于是 ST(意法 半导体)推出了官方固件库,固件库将这些寄存器底层操作都封装起来,提供一整套接口(API) 供开发者调用,大多数场合下,你不需要去知道操作的是哪个寄存器,你只需要知道调用哪些 函数即可。 比如上面的控制 BRR 寄存器实现电平控制,官方库封装了一个函数: void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { GPIOx->BRR = GPIO_Pin; 24 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 } 这个时候你不需要再直接去操作 BRR 寄存器了,你只需要知道怎么使用 GPIO_ResetBits()这个 函数就可以了。在你对外设的工作原理有一定的了解之后,你再去看固件库函数,基本上函数 名字能告诉你这个函数的功能是什么,该怎么使用,这样是不是开发会方便很多? 任何处理器,不管它有多么的高级,归根结底都是要对处理器的寄存器进行操作。但是固 件库不是万能的,您如果想要把 STM32 学透,光读 STM32 固件库是远远不够的。你还是要了 解一下 STM32 的原理,而这些原理了解了,你在进行固件库开发过程中才可能得心应手游刃 有余。 3.1.2 STM32 固件库与 CMSIS 标准讲解 前一节我们讲到,STM32 固件库就是函数的集合,那么对这些函数有什么要求呢??这里 就涉及到一个 CMSIS 标准的基础知识,这部分知识可以从《Cortex-M3 权威指南》中了解到, 我们这里只是对权威指南的讲解做个概括性的介绍。经常有人问到 STM32 和 ARM 以及 ARM7 是什么关系这样的问题,其实 ARM 是一个做芯片标准的公司,它负责的是芯片内核的架构设 计,而 TI,ST 这样的公司,他们并不做标准,他们是芯片公司,他们是根据 ARM 公司提供的 芯片内核标准设计自己的芯片。所以,任何一个做 Cortex-M3 芯片,他们的内核结构都是一样 的,不同的是他们的存储器容量,片上外设,IO 以及其他模块的区别。所以你会发现,不同公 司设计的 Cortex-M3 芯片他们的端口数量,串口数量,控制方法这些都是有区别的,这些资源 他们可以根据自己的需求理念来设计。同一家公司设计的多种 Cortex-m3 内核芯片的片上外设 也会有很大的区别,比如 STM32F103RBT 和 STM32F103ZET,他们的片上外设就有很大的区 别。我们可以通过《Cortex-M3 权威指南》中的一个图来了解一下: 图 3.1.2.1 Cortex-m3 芯片结构 从上图可以看出,芯片虽然是芯片公司设计,但是内核却要服从 ARM 公司提出的 Cortex-M3 内核标准了,理所当然,芯片公司每卖出一片芯片,需要向 ARM 公司交一定的专利费。 既然大家都使用的是 Cortex-M3 核,也就是说,本质上大家都是一样的,这样 ARM 公司 为了能让不同的芯片公司生产的 Cortex-M3 芯片能在软件上基本兼容,和芯片生产商共同提出 25 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 了一套标准 CMSIS 标准(Cortex Microcontroller Software Interface Standard) ,翻译过来是 “ARM Cortex™ 微控制器软件接口标准”。ST 官方库就是根据这套标准设计的。这里我们又 要引用参考资料里面的图片来看看基于 CMSIS 应用程序基本结构: 图 3.1.2.2 基于 CMSIS 应用程序基本结构 CMSIS 分为 3 个基本功能层: 1) 核内外设访问层:ARM 公司提供的访问,定义处理器内部寄存器地址以及功能函数。 2) 中间件访问层:定义访问中间件的通用 API,也是 ARM 公司提供。 3) 外设访问层:定义硬件寄存器的地址以及外设的访问函数。 从图中可以看出,CMSIS 层在整个系统中是处于中间层,向下负责与内核和各个外设直接打交 道,向上提供实时操作系统用户程序调用的函数接口。如果没有 CMSIS 标准,那么各个芯片公 司就会设计自己喜欢的风格的库函数,而 CMSIS 标准就是要强制规定,芯片生产公司设计的库 函数必须按照 CMSIS 这套规范来设计。 其实不用这么讲这么复杂的,一个简单的例子,我们在使用 STM32 芯片的时候首先要进 行系统初始化,CMSIS 规范就规定,系统初始化函数名字必须为 SystemInit,所以各个芯片公 司写自己的库函数的时候就必须用 SystemInit 对系统进行初始化。CMSIS 还对各个外设驱 动文件的文件名字规范化,以及函数名字规范化等等一系列规定。上一节讲的函数 GPIO_ResetBits 这个函数名字也是不能随便定义的,是要遵循 CMSIS 规范的。 至于 CMSIS 的具体内容就不必多讲了,需要了解详细的朋友可以到网上搜索资料,相 关资料可谓满天飞。 3.1.3 STM32 官方库包介绍 这一节内容主要讲解 ST 官方提供的 STM32 固件库包的结构。ST 官方提供的固件库完整包 可以在官方下载,我们光盘也会提供。固件库是不断完善升级的,所以有不同的版本,我们使 用的是 V3.5 版本的固件库 大家可以到光盘目录: 软件资料\STM32 固件库使用参考资料\ 26 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 STM32F10x_StdPeriph_Lib_V3.5.0 下面查看,这在我们论坛有下载。下面看看官方库包的目录 结构: 图 3.1.2.3 官方库包根目录 图 3.1.2.4 官方库目录列表 3.1.3.1 文件夹介绍: Libraries 文件夹下面有 CMSIS 和 STM32F10x_StdPeriph_Driver 两个目录,这两个目录包 含 固 件 库 核 心 的 所 有 子 文 件 夹 和 文 件 。 其 中 CMSIS 目 录 下 面 是 启 动 文 件 , STM32F10x_StdPeriph_Driver 放的是 STM32 固件库源码文件。源文件目录下面的 inc 目录存放 的是 stm32f10x_xxx.h 头文件,无需改动。src 目录下面放的是 stm32f10x_xxx.c 格式的固件库源 码文件。每一个.c 文件和一个相应的.h 文件对应。这里的文件也是固件库的核心文件,每个外 设对应一组文件。 Libraries 文件夹里面的文件在我们建立工程的时候都会使用到。 Project 文件夹下面有两个文件夹。顾名思义,STM32F10x_StdPeriph_Examples 文件夹下面 存放的的 ST 官方提供的固件实例源码,在以后的开发过程中,可以参考修改这个官方提供的 实例来快速驱动自己的外设,很多开发板的实例都参考了官方提供的例程源码,这些源码对以 后的学习非常重要。STM32F10x_StdPeriph_Template 文件夹下面存放的是工程模板。 Utilities 文件下就是官方评估板的一些对应源码,这个可以忽略不看。 根目录中还有一个 stm32f10x_stdperiph_lib_um.chm 文件,直接打开可以知道,这是一个固 件库的帮助文档,这个文档非常有用,只可惜是英文的,在开发过程中,这个文档会经常被使 用到。 27 3.1.3.2 关键文件介绍: STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 下面我们要着重介绍 Libraries 目录下面几个重要的文件。 core_cm3.c 和 core_cm3.h 文件位于\Libraries\CMSIS\CM3\CoreSupport 目录下面的,这个就 是 CMSIS 核心文件,提供进入 M3 内核接口,这是 ARM 公司提供,对所有 CM3 内核的芯片 都一样。你永远都不需要修改这个文件,所以这里我们就点到为止。 和 CoreSupport 同一级还有一个 DeviceSupport 文件夹。DeviceSupport\ST\STM32F10xt 文件 夹下面主要存放一些启动文件以及比较基础的寄存器定义以及中断向量定义的文件。 图 3.1.2.5 DeviceSupport\ST\STM32F10x 目录结构 这个目录下面有三个文件:system_stm32f10x.c, system_stm32f10x.h 以及 stm32f10x.h 文件。其 中 system_stm32f10x.c 和对应的头文件 system_stm32f10x.h 文件的功能是设置系统以及总线时 钟,这个里面有一个非常重要的 SystemInit()函数,这个函数在我们系统启动的时候都会调用, 用来设置系统的整个时钟系统。 stm32f10x.h 这个文件就相当重要了,只要你做 STM32 开发,你几乎时刻都要查看这个文 件相关的定义。这个文件打开可以看到,里面非常多的结构体以及宏定义。这个文件里面主要 是系统寄存器定义申明以及包装内存操作,对于这里是怎样申明以及怎样将内存操作封装起来 的,我们在后面的章节“MDK 中寄存器地址名称映射分析”中会讲到。 在 DeviceSupport\ST\STM32F10x 同一级还有一个 startup 文件夹,这个文件夹里面放的文 件顾名思义是启动文件。在\startup\arm 目录下,我们可以看到 8 个 startup 开头的.s 文件。 图 3.1.2.6 startup 文件 这里之所以有 8 个启动文件,是因为对于不同容量的芯片启动文件不一样。对于 103 系列,主 要是用其中 3 个启动文件: startup_stm32f10x_ld.s: 适用于小容量 产品 startup_stm32f10x_md.s : 适用于中等容量产品 startup_stm32f10x_hd.s: 适用于大容量产品 28 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 这里的容量是指 FLASH 的大小.判断方法如下: 小容量:FLASH≤32K 中容量:64K≤FLASH≤128K 大容量:256K≤FLASH 我们 ALIENTEK miniSTM32 开发板采用的 103RCT6 是属于大容量产品,所以我们的启动文件 选择 startup_stm32f10x_hd.s。 启动文件到底什么作用,其实我们可以打开启动文件进去看看。 启动文件主要是进行堆栈之类的初始化,中断向量表以及中断函数定义。启动文件要引导进入 main 函数。Reset_Handler 中断函数是唯一实现了的中断处理函数,其他的中断函数基本都是死 循环。 Reset_handler 在我们系统启动的时候会调用,下面让我们看看 Reset_handler 这段代码: ; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP 这段代码我也看不懂,反正就知道,这里面要引导进入 main 函数,同时在进入 main 函数之前, 首先要调用 SystemInit 系统初始化函数。 还有其他几个文件 stm32f10x_it.c,stm32f10x_it.h 以及 stm32f10x_conf.h 等文件,这里就不 一一介绍。stm32f10x_it.c 里面是用来编写中断服务函数,中断服务函数也可以随意编写在工程 里面的任意一个文件里面,个人觉得这个文件没太大意义。 stm32f10x_conf.h 文件打开可以看到一堆的#include,这里你建立工程的时候,可以注释掉一 些你不用的外设头文件。这里相信大家一看就明白。 这一节我们就简要介绍到这里,后面我们会介绍怎样建立基于 V3.5 版本固件库的工程模块。 3.2MDK5 简介 MDK 源自德国的 KEIL 公司,是 RealView MDK 的简称。在全球 MDK 被超过 10 万的嵌 入式开发工程师使用。目前最新版本为:MDK5.10,该版本使用 uVision5 IDE 集成开发环境, 是目前针对 ARM 处理器,尤其是 Cortex M 内核处理器的最佳开发工具。 MDK5 向后兼容 MDK4 和 MDK3 等,以前的项目同样可以在 MDK5 上进行开发(但是头文 件方面得全部自己添加), MDK5 同时加强了针对 Cortex-M 微控制器开发的支持,并且对传统 的开发模式和界面进行升级,MDK5 由两个部分组成:MDK Core 和 Software Packs。其中, Software Packs 可以独立于工具链进行新芯片支持和中间库的升级。如图 3.2.1 所示: 29 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.2.1 MDK5 组成 从上图可以看出,MDK Core 又分成四个部分:uVision IDE with Editor(编辑器),ARM C/C++ Compiler(编译器),Pack Installer(包安装器),uVision Debugger with Trace(调试跟踪 器)。uVision IDE 从 MDK4.7 版本开始就加入了代码提示功能和语法动态检测等实用功能,相 对于以往的 IDE 改进很大。 Software Packs(包安装器)又分为:Device(芯片支持),CMSIS(ARM Cortex 微控制器 软件接口标准)和 Mdidleware(中间库)三个小部分,通过包安装器,我们可以安装最新的组 件,从而支持新的器件、提供新的设备驱动库以及最新例程等,加速产品开发进度。 同以往的 MDK 不同,以往的 MDK 把所有组件到包含到了一个安装包里面,显得十分“笨 重”,MDK5 则不一样,MDK Core 是一个独立的安装包,它并不包含器件支持、设备驱动、 CMSIS 等组件,大小不到 300M,相对于 MDK4.70A 的 500 多 M,瘦身明显,MDK5 安装包可 以在:http://www.keil.com/demo/eval/arm.htm 下载到。而器件支持、设备驱动、CMSIS 等组件, 则可以点击 MDK5 的 Build Toolbar 的最后一个图标调出 Pack Installer,来进行各种组件的安装。 也可以在 http://www.keil.com/dd2/pack 这个地址下载,然后进行安装。 最后,我们学习 STM32F103,还要安装两个包:ARM.CMSIS.3.20.4.pack(用于支持 ST 标准库,也就是所谓的库函数)和 Keil.STM32F1xx_DFP.1.0.4.pack(STM32F1 的器件库)。这 两个包以及 MDK5.10 的安装软件,我们都已经在开发板光盘提供了,大家自行安装即可。 3.3 新建基于 V3.5 固件库的 MDK5 工程模板 在前面的章节我们介绍了 STM32 官方库包的一些知识,这些我们将着重讲解建立基于固 件库的工程模板的详细步骤。在此之前,首先我们要准备如下资料: 1) V3.5 固件库包:STM32F10x_StdPeriph_Lib_V3.5.0 这是 ST 官网下载的固件库完 整版,我们光盘目录(压缩包):“8,STM32 参考资料\STM32 固件库使用参考资料”。 我们官方论坛下载地址:http://openedv.com/posts/list/6054.htm 2) MDK5 开发环境(我们的板子的开发环境目前是使用这个版本)。这在我们光盘 的软件目录下面有安装包:软件资料\软件\MDK5。 3) MDK 注册机,这在我们光盘的 MDK 同一目录下面有。 光盘目录:软件资料\软件\MDK5\ keygen.exe。 30 3.3.1 MDK5 安装步骤 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 MDK5 的安装,请参考光盘:1,ALIENTEK MiniSTM32 开发板入门资料\MDK5.10 安装 手册.pdf,里面详细介绍了 MDK5 的安装方法。大家按照手册步骤一步一步安装即可。这里需 要特别说明一下,如果您使用过其他 mdk 或者 keil,请确保新的 mdk5.10 的安装路径跟以前 的版本的 mdk 或者 keil 的安装路径不一样,否则,就会出一些奇怪的错误。 3.3.2 添加 License Key MDK 针对每台机会有一个 CID,copy 这个 CID 到注册机处生成 License Key,然后再将这个 License Key 添加到 MDK 里面去注册。详细步骤后面会一一介绍。 1) 打开运行 MDK。这里要注意,有些版本的 windows 系统(如 Vista)需要右键点击快捷方式选 择“以管理员身份运行”,因为注册 license 需要管理员权限。打开 MDK 后有一个名字叫 “LPC2129 simulator”的默认 Project,暂时我们可以不用理会。 2) 点击:File->License Management,弹出一个 License Management 界面,copy 界面中的 (CID): 图 3.3.2.1 License Management 选择 31 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.2.2 获取 CID 3) 打开光盘(软件资料\软件\MDK5\ keygen.exe)下面的注册机 ,注册机我们会跟 MDK 安装包放在同一目录下面。 4) 接着会出现注册界面,黏贴刚才复制的 CID 到 CID 输入框,然后 Target 选择 ARM 之后, 点击“Generate”,30 位的 License Key 会在下图红色圈出的部分生成。License Key 的 格式:D0DY8-30KAK-0N8AM-X9Z14-A2NWP-J3LZZ。 图 3.3.2.3 生成 License Key 32 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 5) 将这个 License Key 黏贴到 Keil 的 License Management 界面的 New License Id Code 一 栏,然后点击“Add LIC”,添加成功后会出现成功提示。然后点击 Close 关闭这个界面即 可。到此 License Key 便添加完成。添加成功后界面会显示“LIC Added Successfully”。 图 3.3.2.4 添加 License Key 成功 3.3.3 新建工程模板 在新建之前,首先我们要说明一下,这一小节我们新建的工程放在光盘目录:“4,程序源 码\标准例程-V3.5 库函数版本\实验 0 Template 工程模板” 下面,大家在学习新建工程过程中 间遇到一些问题,可以直接打开这个模板,然后对比看看自己哪里没有配置好。 1) 在建立工程之前,我们建议用户在电脑的某个目录下面建立一个文件夹,后面所建立的工 程都可以放在这个文件夹下面,这里我们建立一个文件夹为 Template。 2) 点击 Keil 的菜单:Project –>New Uvision Project ,然后将目录定位到刚才建立的文件夹 Template 之下,在这个目录下面建立子文件夹 USER(我们的代码工程文件都是放在 USER 目录,很多人喜欢新建“Project”目录放在下面,这也是可以的,这个就看个人喜好了), 然后定位到 USER 目录下面,我们的工程文件就都保存到 USER 文件夹下面。工程命名为 Template,点击保存。 33 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.1 新建工程 图 3.3.3.2 定义工程名称 3) 接下来会出现一个选择 Device 的界面,就是选择我们的芯片型号,这里我们定位到 STMicroelectronics 下面的 STM32F103RC(针对我们的 miniSTM32 板子是这个型号)。这里 我们选择 STMicroelectronicsSTM32F1 SeriesSTM32F103STM32F103RCT6(如果使 用的是其他系列的芯片,选择相应的型号就可以了,例如我们的战舰 STM32 开发板是 STM32F103ZE。特别注意:一定要安装对应的器件 pack 才会显示这些内容哦!!)。 34 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.3 选择芯片型号 点击 OK,MDK 会弹出 Manage Run-Time Environment 对话框,如图 3.3.3.4 所示: 图 3.3.3.4 Manage Run-Time Environment 界面 这是 MDK5 新增的一个功能,在这个界面,我们可以添加自己需要的组件,从而方便构建 开发环境,不过这里我们不做介绍。所以在图 3.3.3.4 所示界面,我们直接点击 Cancel,即可, 得到如图 3.3.3.5 所示界面: 35 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.5 工程初步建立 4) 现在我们看看 USER 目录下面包含 2 个文件: 图 3.3.3.6 工程 USER 目录文件 5) 接 下 来 , 我 们 在 Template 工 程 目 录 下 面 , 新 建 3 个 文 件 夹 CORE, OBJ 以 及 STM32F10x_FWLib。CORE 用来存放核心文件和启动文件,OBJ 是用来存放编译过程文件 36 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 以及 hex 文件,STM32F10x_FWLib 文件夹顾名思义用来存放 ST 官方提供的库函数源码文 件。已有的 USER 目录除了用来放工程文件外,还用来存放主函数文件 main.c,以及其他包 括 system_stm32f10x.c 等等。 图 3.3.3.7 工程目录预览 6) 下面我们要将官方的固件库包里的源码文件复制到我们的工程目录文件夹下面。 打开官方固件库包,定位到我们之前准备好的固件库包的目录: STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver 下面, 将目录下面的 src,inc 文件夹 copy 到我们刚才建立的 STM32F10x_FWLib 文件夹下面。 src 存放的是固件库的.c 文件,inc 存放的是对应的.h 文件,您不妨打开这两个文件目录过目一 下里面的文件,每个外设对应一个.c 文件和一个.h 头文件。 图 3.3.3.6 官方库源码文件夹 7) 下面我们要将固件库包里面相关的启动文件复制到我们的工程目录 CORE 之下。 打开官方固件库包,定位到目录 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\CoreSupport 下面,将文件 core_cm3.c 37 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 和 文 件 core_cm3.h 复 制 到 CORE 下 面 去 。 然 后 定 位 到 目 录 STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\a rm 下面,将里面 startup_stm32f10x_hd.s 文件复制到 CORE 下面。这里我们我之前已经解释了 不同容量的芯片使用不同的启动文件,我们的芯片 STM32F103ZET6 是大容量芯片,所以选择 这个启动文件。 现在看看我们的 CORE 文件夹下面的文件: 图 3.3.3.7 启动文件夹 8) 定位到目录: STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x 下面 将里面的三个文件 stm32f10x.h,system_stm32f10x.c,system_stm32f10x.h,复制到我们的 USER 目录之下。然后将 STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template 下 面 的 4 个 文 件 main.c,stm32f10x_conf.h,stm32f10x_it.c,stm32f10x_it.h 复制到 USER 目录下面。 38 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.8 USER 目录文件浏览 9) 前面 8 个步骤,我们将需要的固件库相关文件复制到了我们的工程目录下面,下面我们将 这些文件加入我们的工程中去。右键点击 Target1,选择 Manage Components 39 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.9 点击 Management Components 10) Project Targets 一栏,我们将 Target 名字修改为 Template,然后在 Groups 一栏删掉一个 Source Group1,建立三个 Groups:USER,CORE,FWLIB。然后点击 OK,可以看到我们的 Target 名字以及 Groups 情况。 图 3.3.3.10 40 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.11 11) 下面我们往 Group 里面添加我们需要的文件。我们按照步骤 12 的方法, 右键点击点击 Tempate,选择选择 Manage Components.然后选择需要添加文件的 Group,这里第一步我们 选 择 FWLIB , 然 后 点 击 右 边 的 Add Files, 定 位 到 我 们 刚 才 建 立 的 目 录 STM32F10x_FWLib/src 下面,将里面所有的文件选中(Ctrl+A),然后点击 Add,然后 Close. 可以看到 Files 列表下面包含我们添加的文件。 这里需要说明一下,对于我们写代码,如果我们只用到了其中的某个外设,我们就可以不 用添加没有用到的外设的库文件。例如我只用 GPIO,我可以只用添加 stm32f10x_gpio.c 而 其他的可以不用添加。这里我们全部添加进来是为了后面方便,不用每次添加,当然这样 的坏处是工程太大,编译起来速度慢,用户可以自行选择。 41 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.12 12) 用同样的方法,将 Groups 定位到 CORE 和 USER 下面,添加需要的文件。这里 我们的 CORE 下面需要添加的文件为 core_cm3.c,startup_stm32f10x_hd.s(注意,默认添加的 时候文件类型为.c,也就是添加 startup_stm32f10x_hd.s 启动文件的时候,你需要选择文件类型 为 All files 才能看得到这个文件),USER 目录下面需要添加的文件为 main.c,stm32f10x_it.c, system_stm32f10x.c. 这样我们需要添加的文件已经添加到我们的工程中去了,最后点击 OK,回到工程主界面。 42 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.13 43 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.14 图 3.3.3.15 13) 接下来我们要编译工程,在编译之前我们首先要选择编译中间文件编译后存放目录。 方法是点击魔术棒,然后选择“Output”选项下面的“Select folder for objects…”,然后选 择目录为我们上面新建的 OBJ 目录。 44 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.16 14) 在编译之前,我们先把 main.c 文件里面的内容替换为如下内容: int main(void){ while(1); } 15) 下面我们点击编译按钮 编译工程,可以看到很多报错,因为找不到头文件: 45 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.17 16) 下面我们要告诉 MDK,在哪些路径之下搜索需要的头文件,也就是头文件路径。回到工 程主菜单,点击魔术棒 ,出来一个菜单,然后点击 c/c++选项.然后点击 Include Paths 右边的按钮。弹出一个添加 path 的对话框,然后我们将图上面的 3 个目录添加进去(\USER, \CORE, \STM32F10x_FWLib\inc)。记住,keil 只会在一级目录查找,所以如果你的目录 下面还有子目录,记得 path 一定要定位到最后一级子目录。然后点击 OK. 46 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.18 图 3.3.3.219 47 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.20 17) 接下来,我们再来编译工程,可以看到又报了很多错误。这是因为 3.5 版本的库函数在配 置和选择外设的时候通过宏定义来选择的,所以我们需要配置一个全局的宏定义变量。按 照步骤 16,定位到 c/c++界面,然后填写 “STM32F10X_HD,USE_STDPERIPH_DRIVER”到 Define 输入框里面(请注意,两个标识 符中间是逗号不是句号,如果您不能确定您输入的是正确的,请直接打开我们光盘任何一 个库函数实例,然后复制过来这串文字即可)。这里解释一下,如果你用的是中容量那么 STM32F10X_HD 修改为 STM32F10X_MD,小容量修改为 STM32F10X_LD. 然后点击 OK。 因为我们的 miniSTm32 开发板是大容量,所以要选择“STM32F10X_HD”。 48 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.21 18) 这次在编译之前,我们记得打开工程 USER 下面的 main.c,复制下面代码到 main.c 覆盖已 有代码,然后进行编译(这段代码,大家同样可以打开我们光盘新建好了的工程模板的 main.c,把里面的内容复制过来即可。光盘路径为:\4,程序源码\标准例程-V3.5 库函数 版本\实验 0 Template 工程模板\USER\main.c)。可以看到,这次编译已经成功了。 #include "stm32f10x.h" void Delay(u32 count) { u32 i=0; for(;iPA.8 端口配置 //推挽输出 //IO 口速度为 50MHz 49 STM32 不完全手册(库函数版) GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_8); ALIENTEK MiniSTM32 V3.0 开发板教程 //根据设定参数初始化 GPIOA.8 //PA.8 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_SetBits(GPIOD,GPIO_Pin_2); while(1) { GPIO_ResetBits(GPIOA,GPIO_Pin_8); GPIO_SetBits(GPIOD,GPIO_Pin_2); Delay(3000000); GPIO_SetBits(GPIOA,GPIO_Pin_8); GPIO_ResetBits(GPIOD,GPIO_Pin_2); Delay(3000000); } } //LED1-->PD.2 端口配置, 推挽输出 //推挽输出 ,IO 口速度为 50MHz //PD.2 输出高 图 3.3.3.22 19) 这样一个工程模版建立完毕。下面还需要配置,让编译之后能够生成 hex 文件。同样点击 魔术棒,进入配置菜单,选择 Output。然后勾上下三个选项。 其中 Create HEX file 是编 译生成 hex 文件,Browser Information 是可以查看变量和函数定义。还有就是我们要选择 生产的 hex 文件和项目中间文件放在哪个目录,点击“Select folder for Objects…”定位目 50 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 录,我们的选择定位到上面建立的 OBJ 目录下面。 图 3.3.3.23 20) 重新编译代码,可以看到生成了 hex 文件在 OBJ 目录下面,这个文件我们用 mcuisp 下载 到 mcu 即可。 到这里,一个基于固件库 V3.5 的工程模板就建立了。大家可以参考后面我们 3.4 和 3.5 小 节的内容,将代码下载到开发板,会发现两个 led 灯不停的闪烁。 21) 实际上经过前面 20 个步骤,我们的工程模板已经建立完成。但是在我们 ALIENTEK 提供 的实验中,每个实验都有一个 SYSTEM 文件夹,下面有 3 个子目录分别为 sys,usart,delay, 存放的是我们每个实验都要使用到的共用代码,该代码是由我们 ALIENTEK 编写,该代码 的原理在我们第五章会有详细的讲解,我们这里只是引入到工程中,方便后面的实验建立 工程。 首先,找到我们实验光盘,打开任何一个固件库的实验,可以看到下面有一个 SYSTEM 文 件夹(大家一定要注意,打开库函数版本的实验找到 SYSTEM 文件夹,不要用寄存器版 本的 SYSTEM 文件夹),比如我们打开我们的实验 0 Template 工程模板的工程目录如下: 51 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.24 可以看到有一个 SYSTEM 文件夹,进入 SYSTEM 文件夹,里面有三个子文件夹分别为 delay,sys,usart,每个子文件夹下面都有相应的.c 文件和.h 文件。我们接下来要将这三个目录下 面的代码加入到我们工程中去。 用我们之前讲解步骤 12 的办法,在工程中新建一个组,命名为 SYSTEM,然后加入这三 个文件夹下面的.c 文件分别为 sys.c,delay.c,usart.c,如下图: 图 3.3.3.25 然后点击“OK”之后可以看到工程中多了一个 SYSTEM 组,下面有 3 个.c 文件。 52 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.26 接下来我们将对应的三个目录(sys,usart,delay)加入到 PATH 中去,因为每个目录下面都有相 应的.h 头文件,这请参考步骤 16 即可,加入后的截图是: 53 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.3.3.27 最后点击 OK。这样我们的工程模板就彻底完成了。我们建立好的工程模板在我们光盘的实验 目录里面有,路径为“光盘目录:“4,程序源码\标准例程-V3.5 库函数版本\实验 0 Template 工程模板”,大家可以打开对照一下。完整的工程结构如下图: 图 3.3.3.28 3.4 程序下载与调试 上一节,我们学会了如何在 MDK 下创建 STM32 工程。本节,我们将向读者介绍 STM32 的代码下载以及调试。这里的调试包括了软件仿真和硬件调试(在线调试)。通过本章的学习, 你将了解到:1、STM32 软件仿真;2、STM32 程序下载;3、利用 JLINK 对 STM32 进行下载 与在线调试。 3.4.1 STM32 软件仿真 MDK 的一个强大的功能就是提供软件仿真,通过软件仿真,我们可以发现很多将要出现 的问题,避免了下载到 STM32 里面来查这些错误,这样最大的好处是能很方便的检查程序存 在的问题,因为在 MDK 的仿真下面,你可以查看很多硬件相关的寄存器,通过观察这些寄存 器,你可以知道代码是不是真正有效。另外一个优点是不必频繁的刷机,从而延长了 STM32 的 FLASH 寿命(STM32 的 FLASH 寿命≥1W 次)。当然,软件仿真不是万能的,很多问题还 是要到在线调试才能发现。废话不多说了,接下来我们开始进行软件仿真。 上一章,我们创立了一个工程模板,本节我们将教大家如何在 MDK5 的软件环境下仿真这 个工程,以验证我们代码的正确性。首先这里我们修改一下 main.c 文件的内容如下(下面这段 54 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 代码,我们放在前面我们新建的工程模板的 USER 目录下面的一个 txt 文件里面,路径为: “4,程序源码\标准例程-V3.5 库函数版本\实验 0 Template 工程模板\USER\仿真章节使用的 main 文件源码.txt): #include "sys.h" #include "delay.h" #include "usart.h" int main(void) { u8 t=0; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化为 9600 while(1){ printf("t:%d\n",t); delay_ms(500); t++; } } 在开始软件仿真之前,先检查一下配置是不是正确,在 IDE 里面点击 ,确定 Target 选 项卡内容如图 3.4.1.1 所示(主要检查芯片型号和晶振频率,其他的一般默认就可以): 图 3.4.1.1 Target 选项卡 确认了芯片以及外部晶振频率(8.0Mhz)之后,基本上就确定了 MDK5 软件仿真的硬件环 境了,接下来,我们再点击 Debug 选项卡,设置为如图 3.4.1.2 所示: 55 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.1.2 Debug 选项卡 在图 3.4.1.2 中,选择:Use Simulator,即使用软件仿真。选择:Run to main(),即跳过汇 编代码,直接跳转到 main 函数开始仿真。设置下方的:Dialog DLL 分别为:DARMSTM.DLL 和 TARMSTM.DLL,Parameter 均为:-pSTM32F103RC,用于设置支持 STM32F103RC 的软 硬件仿真(即可以通过 Peripherals 选择对应外设的对话框观察仿真结果)。最后点击 OK,完 成设置。 接下来,我们点击 (开始/停止仿真按钮),开始仿真,出现如图 3.4.1.3 所示界面: 56 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.1.3 开始仿真 可以发现,多出了一个工具条,这就是 Debug 工具条,这个工具条在我们仿真的时候是非 常有用的,下面简单介绍一下 Debug 工具条相关按钮的功能。Debug 工具条部分按钮的功能如 图 3.4.1.4 所示: 图 3.4.1.4 Debug 工具条 复位:其功能等同于硬件上按复位按钮。相当于实现了一次硬复位。按下该按钮之后,代 码会重新从头开始执行。 执行到断点处:该按钮用来快速执行到断点处,有时候你并不需要观看每步是怎么执行的, 而是想快速的执行到程序的某个地方看结果,这个按钮就可以实现这样的功能,前提是你在查 看的地方设置了断点。 挂起:此按钮在程序一直执行的时候会变为有效,通过按该按钮,就可以使程序停止下来, 57 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 进入到单步调试状态。 执行进去:该按钮用来实现执行到某个函数里面去的功能,在没有函数的情况下,是等同 于执行过去按钮的。 执行过去:在碰到有函数的地方,通过该按钮就可以单步执行过这个函数,而不进入这个 函数单步执行。 执行出去:该按钮是在进入了函数单步调试的时候,有时候你可能不必再执行该函数的剩 余部分了,通过该按钮就直接一步执行完函数余下的部分,并跳出函数,回到函数被调用的位 置。 执行到光标处:该按钮可以迅速的使程序运行到光标处,其实是挺像执行到断点处按钮功 能,但是两者是有区别的,断点可以有多个,但是光标所在处只有一个。 汇编窗口:通过该按钮,就可以查看汇编代码,这对分析程序很有用。 观看变量/堆栈窗口:该按钮按下,会弹出一个显示变量的窗口,在里面可以查看各种你想 要看的变量值,也是很常用的一个调试窗口。 串口打印窗口:该按钮按下,会弹出一个类似串口调试助手界面的窗口,用来显示从串口 打印出来的内容。 内存查看窗口:该按钮按下,会弹出一个内存查看窗口,可以在里面输入你要查看的内存 地址,然后观察这一片内存的变化情况。是很常用的一个调试窗口 性能分析窗口:按下该按钮,会弹出一个观看各个函数执行时间和所占百分比的窗口,用 来分析函数的性能是比较有用的。 逻辑分析窗口:按下该按钮会弹出一个逻辑分析窗口,通过 SETUP 按钮新建一些 IO 口, 就可以观察这些 IO 口的电平变化情况,以多种形式显示出来,比较直观。 Debug 工具条上的其他几个按钮用的比较少,我们这里就不介绍了。以上介绍的是比较常 用的,当然也不是每次都用得着这么多,具体看你程序调试的时候有没有必要观看这些东西, 来决定要不要看。 这样,我们在上面的仿真界面里面选内存查看窗口、串口打印窗口。然后调节一下这两个 窗口的位置,如图 3.4.1.5 所示: 58 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.1.5 调出仿真串口打印窗口 我们把光标放到 main.c 的 09 行的空白处,然后双击鼠标左键,可以看到在 09 行的左边出 现了一个红框,即表示设置了一个断点(也可以通过鼠标右键弹出菜单来加入),再次双击则取 消)。 然后我们点击 ,执行到该断点处,如图 3.4.1.6 所示: 59 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.1.6 执行到断点处 我们现在先不忙着往下执行,点击菜单栏的 Peripherals->USARTs->USART 1。可以看到, 有很多外设可以查看,这里我们查看的是串口 1 的情况。如图 3.4.1.7 所示: 60 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.1.7 查看串口 1 相关寄存器 单击 USART1 后会在 IDE 之外出现一个如图 3.4.1.8(a)所示的界面: 图 3.4.1.8 串口 1 各寄存器初始化前后对比 图 3.4.8(a)是 STM32 的串口 1 的默认设置状态,从中可以看到所有与串口相关的寄存器 全部在这上面表示出来了,而且有当前串口的波特率等信息的显示。我们接着单击一下 , 执行完串口初始化函数,得到了如图 3.4.8(b)所示的串口信息。大家可以对比一下这两个图 的区别,就知道在 uart_init(9600);这个函数里面大概执行了哪些操作。 通过图 3.4.8(b),我们可以查看串口 1 的各个寄存器设置状态,从而判断我们写的代码是 61 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 否有问题,只有这里的设置正确了之后,才有可能在硬件上正确的执行。同样这样的方法也可 以适用于很多其他外设,这个读者慢慢体会吧!这一方法不论是在排错还是在编写代码的时候, 都是非常有用的。 然后我们继续单击 按钮,一步步执行,最后就会看到在 USART #1 中打印出相关的信 息,如图 3.4.1.9 所示: 图 3.4.1.9 串口 1 输出信息 图中红色方框内的数据是串口 1 打印出来的,证明我们的仿真是通过的,代码运行时会在 串口 1 不停的输出 t 的值,每 0.5s 执行一次。软件仿真的时间可以在 IDE 的最下面(右下角) 观看到,如图 3.4.1.10 所示。并且 t 自增,与我们预期的一致。再次按下 结束仿真。 图 3.4.1.10 仿真持续时间 至此,我们软件仿真就结束了,通过软件仿真,我们在 MDK5 中验证了代码的正确性,接 下来我们下载代码到硬件上来真正验证一下我们的代码是否在硬件上也是可行的。 3.4.2 STM32 程序下载 STM32 的程序下载有多种方法:USB、串口、JTAG、SWD 等,这几种方式,都可以用来 给 STM32 下载代码。不过,我们最常用的,最经济的,就是通过串口给 STM32 下载代码。本 节,我们将向大家介绍,如何利用串口给 STM32 下载代码。 STM32 的串口下载一般是通过串口 1 下载的, 本指南的实验平台 ALIENTEK miniSTM32 开发板,不是通过 RS232 串口下载的,而是通过自带的 USB 串口来下载。看起来像是 USB 下 62 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 载(只需一根 USB 线,并不需要串口线)的,实际上,是通过 USB 转成串口,然后再下载的。 下面,我们就一步步教大家如何在实验平台上利用 USB 串口来下载代码。 首先要在板子上设置一下,在板子上把 RXD 和 PA(9 STM32 的 TXD),TXD 和 PA10(STM32 的 RXD)通过跳线帽连接起来,这样我们就把 CH340G 和 MCU 的串口 1 连接上了。这里由于 ALIENTEK 这款开发板自带了一键下载电路,所以我们并不需要去关心 BOOT0 和 BOOT1 的 状态,但是为了让下下载完后可以按复位执行程序,我们建议大家把 BOOT1 和 BOOT0 都设置 为 0。设置完成如下图所示: 图 3.4.2.1 开发板串口下载跳线设置 接着我们在 USB_232 处插入 USB 线,并接上电脑,如果之前没有安装 CH340G 的驱动(如 果已经安装过了驱动,则应该能在设备管理器里面看到 USB 串口,如果不能则要先卸载之前的 驱动,卸载完后重启电脑,再重新安装我们提供的驱动),则电脑会提示找到新硬件,如图 3.4.2.2 所示: 图 3.4.2.2 找到新硬件 我们不理会这个提示,直接找到光盘软件资料软件 文件夹下的 CH340 驱动,安装该 驱动,如图 3.4.2.3 所示: 63 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.2.3 CH340 驱动安装 在驱动安装成功之后,拔掉 USB 线,然后重新插入电脑,此时电脑就会自动给其安装驱动 了。在安装完成之后,可以在电脑的设备管理器里面找到 USB 串口(如果找不到,则重启下电 脑),如图 3.4.2.4 所示: 图 3.4.2.4 USB 串口 在图 4.2.4 中可以看到,我们的 USB 串口被识别为 COM3,这里需要注意的是:不同电脑 可能不一样,你的可能是 COM4、COM5 等,但是 USB-SERIAL CH340,这个一定是一样的。 如果没找到 USB 串口,则有可能是你安装有误,或者系统不兼容。 在安装了 USB 串口驱动之后,我们就可以开始串口下载代码了,这里我们的串口下载软件 选 择 的 是 mcuisp , 该 软 件 属 于 第 三 方 软 件 , 由 单 片 机 在 线 编 程 网 提 供 , 大 家 可 以 去 www.mcuisp.com 免费下载,本指南的光盘也附带了这个软件,版本为 V0.993。该软件启动界 面如图 3.4.2.5 所示: 图 3.4.2.5 mcuisp 启动界面 然后我们选择要下载的 Hex 文件,以前面我们新建的工程为例,因为我们前面在工程建立 64 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 的时候,就已经设置了生成 Hex 文件,所以编译的时候已经生成了 Hex 文件,我们只需要找到 这个 Hex 文件下载即可。 用 mcuisp 软件打开 OBJ 文件夹,找到 TEST.hex,打开并进行相应设置后,如图 3.4.2.6 所 示: 图 3.4.2.6 mcuisp 设置 图 4.2.6 中圈中的设置,是我们建议的设置。编程后执行,这个选项在无一键下载功能的条 件下是很有用的,当选中该选项之后,可以在下载完程序之后自动运行代码。否则,还需要按 复位键,才能开始运行刚刚下载的代码。 编程前重装文件,该选项也比较有用,当选中该选项之后,mcuisp 会在每次编程之前,将 hex 文件重新装载一遍,这对于代码调试的时候是比较有用的。特别提醒:不要选择使用 RamIsp, 否则,可能没法正常下载。 最后,我们选择的 DTR 的低电平复位,RTS 高电平进 BootLoader(也就是选项第四项, 这里大家千万注意,很多用户都是这里没有选对),这个选择项选中,mcuisp 就会通过 DTR 和 RTS 信号来控制板载的一键下载功能电路,以实现一键下载功能。如果不选择,则无法实现一 键下载功能。这个是必要的选项(在 BOOT0 接 GND 的条件下)。 在装载了 hex 文件之后,我们要下载代码还需要选择串口,这里 mcuisp 有智能串口搜索功 能。每次打开 mcuisp 软件,软件会自动去搜索当前电脑上可用的串口,然后选中一个作为默认 的串口(一般是你最后一次关闭时所选则的串口)。也可以通过点击菜单栏的搜索串口,来实 现自动搜索当前可用串口。串口波特率则可以通过 bps 那里设置,对于 STM32,该波特率最大 为 230400bps,这里我们一般选择最高的波特率:460800,让 mcuisp 自动去同步。找到 CH340 虚拟的串口,如图 3.4.2.7 所示: 65 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.2.7 CH340 虚拟串口 从之前 USB 串口的安装可知,开发板的 USB 串口被识别为 COM3 了(如果你的电脑是被 识别为其他的串口,则选择相应的串口即可),所以我们选择 COM3。选择了相应串口之后, 我们就可以通过按开始编程(P)这个按钮,一键下载代码到 STM32 上,下载成功后如图 3.4.2.8 所示: 图 3.4.2.8 下载完成 图 4.2.8 中,我们用圈圈圈出了 mcuisp 对一键下载电路的控制过程,其实就是控制 DTR 和 RTS 电平的变化,控制 BOOT0 和 RESET,从而实现自动下载。另外,界面提示已经下载完成 (如果老提示:开始连接…,需要检查一下,开发板的设置是否正确,是否有其他因素干扰等), 并且从 0X80000000 处开始运行了,我们打开串口调试助手选择 COM3,会发现从 ALIENTEK miniSTM32 开发板发回来的信息,如图 3.4.2.9 所示: 66 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.2.9 程序开始运行了 接收到的数据和我们仿真的是一样的,证明程序没有问题。至此,说明我们下载代码成功 了,并且也从硬件上验证了我们代码的正确性。 3.4.3 JLINK 下载与调试程序 上一节,我们介绍了如何通过利用串口给 STM32 下载代码,并在 ALIENTEK miniSTM32 开发板上验证了我们程序的正确性。这个代码比较简单,所以不需要硬件调试,我们直接就一 次成功了。可是,如果你的代码工程比较大,难免存在一些 bug,这时,就有必要通过硬件调 试来解决问题了。 串口只能下载代码,并不能实时跟踪调试,而利用调试工具,比如 JLINK、ULINK、STLINK 等就可以实时跟踪程序,从而找到你程序中的 bug,使你的开发事半功倍。这里我们以 JLINK V8 为例,说说如何在线调试 STM32。 JLINK V8 支持 JTAG 和 SWD,同时 STM32 也支持 JTAG 和 SWD。所以,我们有 2 种方 式可以用来调试,JTAG 调试的时候,占用的 IO 线比较多,而 SWD 调试的时候占用的 IO 线很 少,只需要两根即可。 JLINK V8 的驱动安装比较简单,我们在这里就不说了。在安装了 JLINK V8 的驱动之后, 我们接上 JLINK V8,并把 JTAG 口插到 ALIENTEK miniSTM32 开发板上,打开之前 3.2 节新 建的工程,点击 ,打开 Options for Target 选项卡,在 Debug 栏选择仿真工具为 Cortex-M3 J-LINK,如图 3.4.3.1 所示: 67 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.3.1 Debug 选项卡设置 上图中我们还勾选了 Run to main(),该选项选中后,只要点击仿真就会直接运行到 main 函 数,如果没选择这个选项,则会先执行 startup_stm32f10x_hd.s 文件的 Reset_Handler,再跳到 main 函数。 然后我们点击 Settings,设置 J-LINK 的一些参数,如图 3.4.3.2 所示: 图 3.4.3.2 J-LINK 模式设置 图 4.3.2 中,我们使用 J-LINK V8 的 SW 模式调试,因为我们 JTAG 需要占用比 SW 模式多 68 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 很多的 IO 口,而在 ALIENTEK miniSTM32 开发板上这些 IO 口可能被其他外设用到,可能造 成部分外设无法使用。所以,我们建议大家在调试的时候,一定要选择 SW 模式。Max Clock, 可以点击 Auto Clk 来自动设置,图 4.3.2 中我们设置 SWD 的调试速度为 10Mhz,这里,如果你 的 USB 数据线比较差,那么可能会出问题,此时,你可以通过降低这里的速率来试试。 单击 OK,完成此部分设置,接下来我们还需要在 Utilities 选项卡里面设置下载时的目标编 程器,如图 3.4.3.3 所示: 图 3.4.3.3 FLASH 编程器选择 图 3.4.3.3 中,我们选择 J-LINK 来调试 Cortex M3,然后点击 Settings,设置如图 3.4.3.4 所 示: 69 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.3.4 编程设置 这里要根据不同的 MCU 选择 FLASH 的大小,因为我们开发板使用的是 STM32F103ZET6, 其 FLASH 大小为 512KB,所以我们点击 Add,并在 Programming Algorithm 里面选择 512K 型 号的 STM32。然后选中 Reset and Run 选项,以实现在编程后自动启动,其他默认设置即可。 设置完成之后,如图 3.4.3.4 所示。 在设置完之后,点击 OK,然后再点击 OK,回到 IDE 界面,编译一下工程。接下来我们 就可以通过 JLINK 下载代码和调试代码。 配置好 JLINK 之后,使用 JLINK 下载代码就非常简单,大家只需要点击 LOAD 按钮就可 以进行程序下载。下载完成之后程序就可以直接在开发板执行。如图 3.4.3.5: 如图 3.4.3.5 接下来我们看看用 JLINK 进行程序仿真。点击 ,开始仿真(如果开发板的代码没被更 新过,则会先更新代码,再仿真,你也可以通过按 ,只下载代码,而不进入仿真。特别注意: 开发板上的 B0 和 B1 都要设置到 GND,否则代码下载后不会自动运行的!),如图 3.4.3.6 所示: 70 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.3.6 开始仿真 因为我们之前勾选了 Run to main()选项,所以,程序直接就运行到了 main 函数的入口处, 我们在 delay_init()处设置了一个断点,点击 ,程序将会快速执行到该处。如图 3.4.3.7 所示: 71 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.4.3.7 程序运行到断点处 接下来,我们就可以和软件仿真一样的开始操作了,不过这是真正的在硬件上的运行,而 不是软件仿真!其结果更可信。硬件调试就给大家介绍到这里。 3.5 MDK5 使用技巧 通过前面的学习,我们已经了解了如何在 MDK5 里面建立属于自己的工程。下面,我们将 向大家介绍 MDK5 软件的一些使用技巧,这些技巧在代码编辑和编写方面会非常有用,希望大 家好好掌握,最好实际操作一下,加深印象。 3.5.1 文本美化 文本美化,主要是设置一些关键字、注释、数字等的颜色和字体。前面我们在介绍 MDK5 新建工程的时候看到界面如图 3.2.22 所示,这是 MDK 默认的设置,可以看到其中的关键字和 注释等字体的颜色不是很漂亮,而 MDK 提供了我们自定义字体颜色的功能。我们可以在工具 条上点击 (配置对话框)弹出如图 3.5.1.2 所示界面: 图 3.5.1.1 置对话框 在该对话框中,先设置 Encoding 为:Chinese GB2312(Simplified),然后设置 Tab size 为:4。 以更好的支持简体中文(否则,拷贝到其他地方的时候,中文可能是一堆的问号),同时 TAB 间隔设置为 4 个单位。然后,选择:Colors&Fonts 选项卡,在该选项卡内,我们就可以设置自 己的代码的子体和颜色了。由于我们使用的是 C 语言,故在 Window 下面选择:C/C++ Editor Files 在右边就可以看到相应的元素了。如图 3.5.1.2 示: 72 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.5.1.2 Colors&Fonts 选项卡 然后点击各个元素修改为你喜欢的颜色(注意双击,且有时候可能需要设置多次才生效, MDK 的 bug),当然也可以在 Font 栏设置你字体的类型,以及字体的大小等。设置成之后,点 击 OK,就可以在主界面看到你所修改后的结果,例如我修改后的代码显示效果如图 3.5.3 示: 图 3.5.1.3 设置完后显示效果 这就比开始的效果好看一些了。字体大小,则可以直接按住:ctrl+鼠标滚轮,进行放大或 者缩小,或者也可以在刚刚的配置界面设置字体大小。 细心的读者可能会发现,上面的代码里面有一个 u8,还是黑色的,这是一个用户自定义的 关键字,为什么不显示蓝色(假定刚刚已经设置了用户自定义关键字颜色为蓝色)呢?这就又 要回到我们刚刚的配置对话框了,单这次我们要选择 User Keywords 选项卡,同样选择:C/C++ Editor Files,在右边的 User Keywords 对话框下面输入你自己定义的关键字,如图 3.5.1.4 示: 73 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.5.1.4 用户自定义关键字 图 3.3.5 中我定义了 u8、u16、u32 等 3 个关键字,这样在以后的代码编辑里面只要出现这 三个关键字,肯定就会变成蓝色。点击 OK,再回到主界面,可以看到 u8 变成了蓝色了,如图 3.3.1.5 示: 图 3.5.1.5 设置完后显示效果 其实这个编辑配置对话框里面,还可以对其他很多功能进行设置,比如动态语法检测等, 我们将 3.3.2 节介绍。 3.5.2 语法检测&代码提示 MDK4.70 以上的版本,新增了代码提示与动态语法检测功能,使得 MDK 的编辑器越来越 好用了,这里我们简单说一下如何设置,同样,点击 ,打开配置对话框,选择 Text Completion 选项卡,如图 3.5.2.1 所示: 74 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.5.2.1 Text Completion 选项卡设置 Strut/Class Members,用于开启结构体/类成员提示功能。 Function Parameters,用于开启函数参数提示功能。 Symbols after xx characters,用于开启代码提示功能,即在输入多少个字符以后,提示匹配 的内容(比如函数名字、结构体名字、变量名字等),这里默认设置 3 个字符以后,就开始提示。 如图 3.5.2.2 所示: 图 3.5.2.2 代码提示 Dynamic Syntax Checking,则用于开启动态语法检测,比如编写的代码存在语法错误的时 候,会在对应行前面出现 图标,如出现警告,则会出现 图标,将鼠标光标放图标上面,则 会提示产生的错误/警告的原因,如图 3.5.2.3 所示: 75 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.5.2.3 语法动态检测功能 这几个功能,对我们编写代码很有帮助,可以加快代码编写速度,并且及时发现各种问题。 不过这里要提醒大家,语法动态检测这个功能,有的时候会误报(比如 sys.c 里面,就有很多 误报),大家可以不用理会,只要能编译通过(0 错误,0 警告),这样的语法误报,一般直接 忽略即可。 3.5.3 代码编辑技巧 这里给大家介绍几个我常用的技巧,这些小技巧能给我们的代码编辑带来很大的方便,相 信对你的代码编写一定会有所帮助。 1)TAB 键的妙用 首先要介绍的就是 TAB 键的使用,这个键在很多编译器里面都是用来空位的,每按一下移 空几个位。如果你是经常编写程序的对这个键一定再熟悉不过了。但是 MDK 的 TAB 键和一般 编译器的 TAB 键有不同的地方,和 C++的 TAB 键差不多。MDK 的 TAB 键支持块操作。也就 是可以让一片代码整体右移固定的几个位,也可以通过 SHIFT+TAB 键整体左移固定的几个位。 假设我们前面的串口 1 中断响应函数如图 3.5.3.1 所示: 图 3.5.3.1 头大的代码 76 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.5.3.1 中这样的代码大家肯定不会喜欢,这还只是短短的 30 来行代码,如果你的代码 有几千行,全部是这个样子,不头大才怪。看到这样的代码我们就可以通过 TAB 键的妙用来快 速修改为比较规范的代码格式。 选中一块然后按 TAB 键,你可以看到整块代码都跟着右移了一定距离,如图 3.5.3.2 所示: 图 3.5.3.2 代码整体偏移 接下来我们就是要多选几次,然后多按几次 TAB 键就可以达到迅速使代码规范化的目的, 最终效果如图 3.5.3.3 所示 图 3.5.3.3 修改后的代码 图 3.5.3.3 中的代码相对于图 3.5.3.1 中的要好看多了,经过这样的整理之后,整个代码一下 就变得有条理多了,看起来很舒服。 2) 快速定位函数/变量被定义的地方 上一节,我们介绍了 TAB 键的功能,接下来我们介绍一下如何快速查看一个函数或者变量 所定义的地方。 77 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 大家在调试代码或编写代码的时候,一定有想看看某个函数是在那个地方定义的,具体里 面的内容是怎么样的,也可能想看看某个变量或数组是在哪个地方定义的等。尤其在调试代码 或者看别人代码的时候,如果编译器没有快速定位的功能的时候,你只能慢慢的自己找,代码 量比较少还好,如果代码量一大,那就郁闷了,有时候要花很久的时间来找这个函数到底在哪 里。型号 MDK 提供了这样的快速定位的功能。只要你把光标放到这个函数/变量(xxx)的上 面(xxx 为你想要查看的函数或变量的名字),然后右键,弹出如图 3.5.3.4 所示的菜单栏 : 图 3.5.3.4 快速定位 在图 3.5.3.4 中,我们找到 Go to Definition Of‘STM32_Clock_Init’ 这个地方,然后单击 左键就可以快速跳到 STM32_Clock_Init 函数的定义处(注意要先在 Options for Target 的 Output 选项卡里面勾选 Browse Information 选项,再编译,再定位,否则无法定位!)。如图 3.5.3.5 所 示: 图 3.5.3.5 定位结果 对于变量,我们也可以按这样的操作快速来定位这个变量被定义的地方,大大缩短了你查 找代码的时间。细心的大家会发现上面还有一个类似的选项,就是 Go to Reference To ‘STM32_Clock_Init’,这个是快速跳到该函数被声明的地方,有时候也会用到,但不如前者使 用得多。 78 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 很多时候,我们利用 Go to Definition/ Reference 看完函数/变量的定义/申明后,又想返回之 前的代码继续看,此时我们可以通过 IDE 上的 按钮(Back to previous position)快速的返回 之前的位置,这个按钮非常好用! 3) 快速注释与快速消注释 接下来,我们介绍一下快速注释与快速消注释的方法。在调试代码的时候,你可能会想注 释某一片的代码,来看看执行的情况,MDK 提供了这样的快速注释/消注释块代码的功能。也 是通过右键实现的。这个操作比较简单,就是先选中你要注释的代码区,然后右键,选择 AdvancedComment Selection 就可以了。 以 Stm32_Clock_Init 函数为例,比如我要注释掉下图中所选中区域的代码,如图 3.5.3.6 所 示: 图 3.5.3.6 选中要注释的区域 我们只要在选中了之后,选择右键,再选择 AdvancedComment Selection 就可以把这段代 码注释掉了。执行这个操作以后的结果如图 3.5.3.7 所示: 图 3.5.3.7 注释完毕 这样就快速的注释掉了一片代码,而在某些时候,我们又希望这段注释的代码能快速的取 79 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 消注释,MDK 也提供了这个功能。与注释类似,先选中被注释掉的地方,然后通过右键 Advanced,不过这里选择的是 Uncomment Selection。 3.5.4 其他小技巧 除了前面介绍的几个比较常用的技巧,这里还介绍几个其他的小技巧,希望能让你的代码 编写如虎添翼。 第一个是快速打开头文件。在将光标放到要打开的引用头文件上,然后右键选择 Open Document“XXX”,就可以快速打开这个文件了(XXX 是你要打开的头文件名字)。如图 3.5.4.1 所示: 图 3.5.4.1 快速打开头文件 第二个小技巧是查找替换功能。这个和 WORD 等很多文档操作的替换功能是差不多的, 在 MDK 里面查找替换的快捷键是“CTRL+H”,只要你按下该按钮就会调出如图 3.5.4.2 所示界 面: 图 3.5.4.2 替换文本 这个替换的功能在有的时候是很有用的,它的用法与其他编辑工具或编译器的差不多,相 信各位都不陌生了,这里就不啰唆了。 第三个小技巧是跨文件查找功能,先双击你要找的函数/变量名(这里我们还是以系统时钟 初始化函数:Stm32_Clock_Init 为例),然后再点击 IDE 上面的 ,弹出如图 3.5.4.3 所示对话 框: 80 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 3.5.4.3 跨文件查找 点击 Find All,MDK 就会帮你找出所有含有 Stm32_Clock_Init 字段的文件并列出其所在位 置,如图 3.5.3.5 所示: 图 3.5.4.4 查找结果 该方法可以很方便的查找各种函数/变量,而且可以限定搜索范围(比如只查找.c 文件和.h 文件等),是非常实用的一个技巧。 81 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第四章 STM32 开发基础知识入门 这一章,我们将着重 STM32 开发的一些基础知识,让大家对 STM32 开发有一个初步的了 解,为后面 STM32 的学习做一个铺垫,方便后面的学习。这一章的内容大家第一次看的时候 可以只了解一个大概,后面需要用到这方面的知识的时候再回过头来仔细看看。这章我们分 7 个小结, ·4.1 MDK 下 C 语言基础复习 ·4.2 STM32 系统架构 ·4.3 STM32 时钟系统 ·4.4 端口复用和重映射 ·4.5 STM32 NVIC 中断管理 ·4.6 MDK 中寄存器地址名称映射分析 ·4.7 MDK 固件库快速开发技巧 4.1 MDK 下 C 语言基础复习 这一节我们主要讲解一下 C 语言基础知识。C 语言知识博大精深,也不是我们三言两语能 讲解清楚,同时我们相信学 STM32 这种级别 MCU 的用户,C 语言基础应该都是没问题的。我们 这里主要是简单的复习一下几个 C 语言基础知识点,引导那些 C 语言基础知识不是很扎实的用 户能够快速开发 STM32 程序。同时希望这些用户能够多去复习一下 C 语言基础知识,C 语言毕 竟是单片机开发中的必备基础知识。对于 C 语言基础比较扎实的用户,这部分知识可以忽略不 看。 4.1.1 位操作 C 语言位操作相信学过 C 语言的人都不陌生了,简而言之,就是对基本类型变量可以在位级 别进行操作。这节的内容很多朋友都应该很熟练了,我这里也就点到为止,不深入探讨。下面 我们先讲解几种位操作符,然后讲解位操作使用技巧。 C 语言支持如下 6 中位操作 运算符 含义 运算符 含义 & 按位与 ~ 取反 | 按位或 << 左移 ^ 按位异或 >> 右移 表 4.1.1.16 种位操作 这些与或非,取反,异或,右移,左移这些到底怎么回事,这里我们就不多做详细,相信 大家学 C 语言的时候都学习过了。如果不懂的话,可以百度一下,非常多的知识讲解这些操作 符。下面我们想着重讲解位操作在单片机开发中的一些实用技巧。 1) 不改变其他位的值的状况下,对某几个位进行设值。 这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作, 然后用|操作符设值。比如我要改变 GPIOA 的状态,可以先对寄存器的值进行&清零操作 GPIOA->CRL&=0XFFFFFF0F; //将第 4-7 位清 0 然后再与需要设置的值进行|或运算 GPIOA->CRL|=0X00000040; //设置相应位的值,不改变其他位的值 2) 移位操作提高代码的可读性。 82 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 移位操作在单片机开发中也非常重要,下面让我们看看固件库的 GPIO 初始化的函数里 面的一行代码 GPIOx->BSRR = (((uint32_t)0x01) << pinpos); 这个操作就是将 BSRR 寄存器的第 pinpos 位设置为 1,为什么要通过左移而不是直接设 置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以 很直观明了的知道,是将第 pinpos 位设置为 1。如果你写成 GPIOx->BSRR =0x0030; 这样的代码就不好看也不好重用了。 类似这样的代码很多: GPIOA->ODR|=1<<5; //PA.5 输出高,不改变其他位 这样我们一目了然,5 告诉我们是第 5 位也就是第 6 个端口,1 告诉我们是设置为 1 了。 3) ~取反操作使用技巧 SR 寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为 0,同时 其他位都保留为 1,简单的作法是直接给寄存器设置一个值: TIMx->SR=0xFFF7; 这样的作法设置第 3 位为 0,但是这样的作法同样不好看,并且可读性很差。看看库函数 代码中怎样使用的: TIMx->SR = (uint16_t)~TIM_FLAG; 而 TIM_FLAG 是通过宏定义定义的值: #define TIM_FLAG_Update ((uint16_t)0x0001) #define TIM_FLAG_CC1 ((uint16_t)0x0002) 看这个应该很容易明白,可以直接从宏定义中看出 TIM_FLAG_Update 就是设置的第 0 位了, 可读性非常强。 4.1.2 define 宏定义 define 是 C 语言中的预处理命令,它用于宏定义,可以提高源代码的可读性,为编程提供 方便。常见的格式: #define 标识符 字符串 “标识符”为所定义的宏名。“字符串”可以是常数、表达式、格式串等。例如: #define SYSCLK_FREQ_72MHz 72000000 定义标识符 SYSCLK_FREQ_72MHz 的值为 72000000。 至于 define 宏定义的其他一些知识,比如宏定义带参数这里我们就不多讲解。 4.1.3 ifdef 条件编译 单片机程序开发过程中,经常会遇到一种情况,当满足某条件时对一组语句进行编译,而 当条件不满足时则编译另一组语句。条件编译命令最常见的形式为: #ifdef 标识符 程序段 1 #else 程序段 2 #endif 它的作用是:当标识符已经被定义过(一般是用#define 命令定义),则对程序段 1 进行编译, 否则编译程序段 2。 其中#else 部分也可以没有,即: 83 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #ifdef 程序段 1 #endif 这个条件编译在 MDK 里面是用得很多的,在 stm32f10x.h 这个头文件中经常会看到这样的语句: #ifdef STM32F10X_HD 大容量芯片需要的一些变量定义 #end 而 STM32F10X_HD 则是我们通过#define 来定义的。条件编译也是 c 语言的基础知识,这里也就 点到为止吧。 4.1.4 extern 变量申明 C 语言中 extern 可以置于变量或者函数前,以表示变量或者函数的定义在别的文件中,提示编 译器遇到此变量和函数时在其他模块中寻找其定义。这里面要注意,对于 extern 申明变量可以多 次,但定义只有一次。在我们的代码中你会看到看到这样的语句: extern u16 USART_RX_STA; 这个语句是申明 USART_RX_STA 变量在其他文件中已经定义了,在这里要使用到。所以,你肯定 可以找到在某个地方有变量定义的语句: u16 USART_RX_STA; 的出现。下面通过一个例子说明一下使用方法。 在 Main.c 定义的全局变量 id,id 的初始化都是在 Main.c 里面进行的。 Main.c 文件 u8 id;//定义只允许一次 main() { id=1; printf("d%",id);//id=1 test(); printf("d%",id);//id=2 } 但是我们希望在 test.c 的 changeId(void)函数中使用变量 id,这个时候我们就需要在 test.c 里面去申明变量 id 是外部定义的了,因为如果不申明,变量 id 的作用域是到不了 test.c 文件 中。看下面 test.c 中的代码: extern u8 id;//申明变量 id 是在外部定义的,申明可以在很多个文件中进行 void test(void){ id=2; } 在 test.c 中申明变量 id 在外部定义,然后在 test.c 中就可以使用变量 id 了。 对于 extern 申明函数在外部定义的应用,这里我们就不多讲解了。 4.1.5 typedef 类型别名 typedef 用于为现有类型创建一个新的名字,或称为类型别名,用来简化变量的定义。 typedef 在 MDK 用得最多的就是定义结构体的类型别名和枚举类型了。 struct _GPIO 84 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { __IO uint32_t CRL; __IO uint32_t CRH; … }; 定义了一个结构体 GPIO,这样我们定义变量的方式为: struct _GPIO GPIOA;//定义结构体变量 GPIOA 但是这样很繁琐,MDK 中有很多这样的结构体变量需要定义。这里我们可以为结体定义一个别 名 GPIO_TypeDef,这样我们就可以在其他地方通过别名 GPIO_TypeDef 来定义结构体变量了。 方法如下: typedef struct { __IO uint32_t CRL; __IO uint32_t CRH; … } GPIO_TypeDef; Typedef 为结构体定义一个别名 GPIO_TypeDef,这样我们可以通过 GPIO_TypeDef 来定义结构体 变量: GPIO_TypeDef _GPIOA,_GPIOB; 这里的 GPIO_TypeDef 就跟 struct _GPIO 是等同的作用了。 这样是不是方便很多? 4.1.6 结构体 经常很多用户提到,他们对结构体使用不是很熟悉,但是 MDK 中太多地方使用结构体以及 结构体指针,这让他们一下子摸不着头脑,学习 STM32 的积极性大大降低,其实结构体并不是 那么复杂,这里我们稍微提一下结构体的一些知识,还有一些知识我们会在下一节的“寄存器 地址名称映射分析”中讲到一些。 声明结构体类型: Struct 结构体名{ 成员列表; }变量名列表; 例如: Struct U_TYPE { Int BaudRate Int WordLength; }usart1,usart2; 在结构体申明的时候可以定义变量,也可以申明之后定义,方法是: Struct 结构体名字 结构体变量列表 ; 例如:struct U_TYPE usart1,usart2; 结构体成员变量的引用方法是: 结构体变量名字.成员名 比如要引用 usart1 的成员 BaudRate,方法是:usart1.BaudRate; 结构体指针变量定义也是一样的,跟其他变量没有啥区别。 例如:struct U_TYPE *usart3;//定义结构体指针变量 usart1; 85 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 结构体指针成员变量引用方法是通过“->”符号实现,比如要访问 usart3 结构体指针指向的结 构体的成员变量 BaudRate,方法是: Usart3->BaudRate; 上面讲解了结构体和结构体指针的一些知识,其他的什么初始化这里就不多讲解了。讲到这里, 有人会问,结构体到底有什么作用呢?为什么要使用结构体呢?下面我们将简单的通过一个实 例回答一下这个问题。 在我们单片机程序开发过程中,经常会遇到要初始化一个外设比如串口,它的初始化状态 是由几个属性来决定的,比如串口号,波特率,极性,以及模式。对于这种情况,在我们没有 学习结构体的时候,我们一般的方法是: void USART_Init(u8 usartx,u32 u32 BaudRate,u8 parity,u8 mode); 这种方式是有效的同时在一定场合是可取的。但是试想,如果有一天,我们希望往这个函数里 面再传入一个参数,那么势必我们需要修改这个函数的定义,重新加入字长这个入口参数。于 是我们的定义被修改为: void USART_Init (u8 usartx,u32 BaudRate, u8 parity,u8 mode,u8 wordlength ); 但是如果我们这个函数的入口参数是随着开发不断的增多,那么是不是我们就要不断的修改函 数的定义呢?这是不是给我们开发带来很多的麻烦?那又怎样解决这种情况呢? 这样如果我们使用到结构体就能解决这个问题了。我们可以在不改变入口参数的情况下, 只需要改变结构体的成员变量,就可以达到上面改变入口参数的目的。 结构体就是将多个变量组合为一个有机的整体。上面的函数,BaudRate,wordlength, Parity,mode,wordlength 这些参数,他们对于串口而言,是一个有机整体,都是来设置串口参 数的,所以我们可以将他们通过定义一个结构体来组合在一个。MDK 中是这样定义的: typedef struct { uint32_t USART_BaudRate; uint16_t USART_WordLength; uint16_t USART_StopBits; uint16_t USART_Parity; uint16_t USART_Mode; uint16_t USART_HardwareFlowControl; } USART_InitTypeDef; 于是,我们在初始化串口的时候入口参数就可以是 USART_InitTypeDef 类型的变量或者指针变 量了,MDK 中是这样做的: void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct); 这样,任何时候,我们只需要修改结构体成员变量,往结构体中间加入新的成员变量,而不需 要修改函数定义就可以达到修改入口参数同样的目的了。这样的好处是不用修改任何函数定义 就可以达到增加变量的目的。 理解了结构体在这个例子中间的作用吗?在以后的开发过程中,如果你的变量定义过多, 如果某几个变量是用来描述某一个对象,你可以考虑将这些变量定义在结构体中,这样也许可 以提高你的代码的可读性。 使用结构体组合参数,可以提高代码的可读性,不会觉得变量定义混乱。当然结构体的作 用就远远不止这个了,同时,MDK 中用结构体来定义外设也不仅仅只是这个作用,这里我们只 是举一个例子,通过最常用的场景,让大家理解结构体的一个作用而已。后面一节我们还会讲 解结构体的一些其他知识。 86 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 4.2 STM32 系统架构 STM32 的系统架构比 51 单片机就要强大很多了。STM32 系统架构的知识可以在《STM32 中文参考手册 V10》的 P25~28 有讲解,这里我们也把这一部分知识抽取出来讲解,是为了大 家在学习 STM32 之前对系统架构有一个初步的了解。这里的内容基本也是从中文参考手册中 参考过来的,让大家能通过我们手册也了解到,免除了到处找资料的麻烦吧。如果需要详细深 入的了解 STM32 的系统架构,还需要在网上搜索其他资料学习学习。 我们这里所讲的 STM32 系统架构主要针对的 STM32F103 这些非互联型芯片。首先我们看 看 STM32 的系统架构图: 图 4.2.1STM32 系统架构图 STM32 主系统主要由四个驱动单元和四个被动单元构成。 四个驱动单元是: 内核 DCode 总线; 系统总线; 通用 DMA1; 通用 DMA2; 四被动单元是: AHB 到 APB 的桥:连接所有的 APB 设备; 内部 FlASH 闪存; 内部 SRAM; FSMC; 87 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 下面我们具体讲解一下图中几个总线的知识: ① ICode 总线:该总线将 M3 内核指令总线和闪存指令接口相连,指令的预取在该总线上 面完成。 ② DCode 总线:该总线将 M3 内核的 DCode 总线与闪存存储器的数据接口相连接,常量 加载和调试访问在该总线上面完成。 ③ 系统总线:该总线连接 M3 内核的系统总线到总线矩阵,总线矩阵协调内核和 DMA 间 访问。 ④ DMA 总线:该总线将 DMA 的 AHB 主控接口与总线矩阵相连,总线矩阵协调 CPU 的 DCode 和 DMA 到 SRAM,闪存和外设的访问。 ⑤ 总线矩阵:总线矩阵协调内核系统总线和 DMA 主控总线之间的访问仲裁,仲裁利用 轮换算法。 ⑥ AHB/APB 桥:这两个桥在 AHB 和 2 个 APB 总线间提供同步连接,APB1 操作速度限于 36MHz,APB2 操作速度全速。 对于系统架构的知识,在刚开始学习 STM32 的时候只需要一个大概的了解,大致知道是个 什么情况即可。对于寻址之类的知识,这里就不做深入的讲解,中文参考手册都有很详细的讲 解。 4.3 STM32 时钟系统 STM32 时钟系统的知识在《STM32 中文参考手册 V10》中的 P55~P73 有非常详细的讲解, 网上关于时钟系统的讲解也基本都是参考的这里,讲不出啥特色,不过作为一个完整的参考手 册,我们必然要提到时钟系统的知识。这些知识也不是什么原创,纯粹的是看网友发的帖子和 手册来总结的,有一些直接是 copy 过来的,望大家谅解。 众所周知,时钟系统是 CPU 的脉搏,就像人的心跳一样。所以时钟系统的重要性就不言而 喻了。 STM32 的时钟系统比较复杂,不像简单的 51 单片机一个系统时钟就可以解决一切。于 是有人要问,采用一个系统时钟不是很简单吗?为什么 STM32 要有多个时钟源呢? 因为首先 STM32 本身非常复杂,外设非常的多,但是并不是所有外设都需要系统时钟这么高的频率,比 如看门狗以及 RTC 只需要几十 k 的时钟即可。同一个电路,时钟越快功耗越大,同时抗电磁干 扰能力也会越弱,所以对于较为复杂的 MCU 一般都是采取多时钟源的方法来解决这些问题。 首先让我们来看看 STM32 的时钟系统图吧: 88 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 4.3.1STM32 时钟系统图 在 STM32 中,有五个时钟源,为 HSI、HSE、LSI、LSE、PLL。从时钟频率来分可以分为 高速时钟源和低速时钟源,在这 5 个中 HIS,HSE 以及 PLL 是高速时钟,LSI 和 LSE 是低速时 钟。从来源可分为外部时钟源和内部时钟源,外部时钟源就是从外部通过接晶振的方式获取时 钟源,其中 HSE 和 LSE 是外部时钟源,其他的是内部时钟源。下面我们看看 STM32 的 5 个时 钟源,我们讲解顺序是按图中红圈标示的顺序: ①、HSI 是高速内部时钟,RC 振荡器,频率为 8MHz。 ②、HSE 是高速外部时钟,可接石英/陶瓷谐振器,或者接外部时钟源,频率范围为 4MHz~16MHz。 我们的开发板接的是 8M 的晶振。 ③、LSI 是低速内部时钟,RC 振荡器,频率为 40kHz。独立看门狗的时钟源只能是 LSI,同 时 LSI 还可以作为 RTC 的时钟源。 ④、LSE 是低速外部时钟,接频率为 32.768kHz 的石英晶体。这个主要是 RTC 的时钟源。 ⑤、PLL 为锁相环倍频输出,其时钟输入源可选择为 HSI/2、HSE 或者 HSE/2。倍频可选择为 89 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 2~16 倍,但是其输出频率最大不得超过 72MHz。 上面我们简要概括了 STM32 的时钟源,那么这 5 个时钟源是怎么给各个外设以及系统提 供时钟的呢?这里我们将一一讲解。我们还是从图的下方讲解起吧,因为下方比较简单。 图中我们用 A~E 标示我们要讲解的地方。 A. MCO 是 STM32 的一个时钟输出 IO(PA8),它可以选择一个时钟信号输出,可以 选择为 PLL 输出的 2 分频、HSI、HSE、或者系统时钟。这个时钟可以用来给外 部其他系统提供时钟源。 B. 这里是 RTC 时钟源,从图上可以看出,RTC 的时钟源可以选择 LSI,LSE,以及 HSE 的 128 分频。 C. 从图中可以看出 C 处 USB 的时钟是来自 PLL 时钟源。STM32 中有一个全速功能 的 USB 模块,其串行接口引擎需要一个频率为 48MHz 的时钟源。该时钟源只能 从 PLL 输出端获取,可以选择为 1.5 分频或者 1 分频,也就是,当需要使用 USB 模块时,PLL 必须使能,并且时钟频率配置为 48MHz 或 72MHz。 D. D 处就是 STM32 的系统时钟 SYSCLK,它是供 STM32 中绝大部分部件工作的时 钟源。系统时钟可选择为 PLL 输出、HSI 或者 HSE。系统时钟最大频率为 72MHz, 当然你也可以超频,不过一般情况为了系统稳定性是没有必要冒风险去超频的。 E. 这里的 E 处是指其他所有外设了。从时钟图上可以看出,其他所有外设的时钟最 终来源都是 SYSCLK。SYSCLK 通过 AHB 分频器分频后送给各模块使用。这些 模块包括: ①、AHB 总线、内核、内存和 DMA 使用的 HCLK 时钟。 ②、通过 8 分频后送给 Cortex 的系统定时器时钟,也就是 systick 了。 ③、直接送给 Cortex 的空闲运行时钟 FCLK。 ④、送给 APB1 分频器。APB1 分频器输出一路供 APB1 外设使用(PCLK1,最大 频率 36MHz),另一路送给定时器(Timer)2、3、4 倍频器使用。 ⑤、送给 APB2 分频器。APB2 分频器分频输出一路供 APB2 外设使用(PCLK2, 最大频率 72MHz),另一路送给定时器(Timer)1 倍频器使用。 其中需要理解的是 APB1 和 APB2 的区别,APB1 上面连接的是低速外设,包括电源接口、 备份接口、CAN、USB、I2C1、I2C2、UART2、UART3 等等,APB2 上面连接的是高速外设包 括 UART1、SPI1、Timer1、ADC1、ADC2、所有普通 IO 口(PA~PE)、第二功能 IO 口等。居宁 老师的《稀里糊涂玩 STM32》资料里面教大家的记忆方法是 2>1, APB2 下面所挂的外设的时 钟要比 APB1 的高。 在以上的时钟输出中,有很多是带使能控制的,例如 AHB 总线时钟、内核时钟、各种 APB1 外设、APB2 外设等等。当需要使用某模块时,记得一定要先使能对应的时钟。后面我们讲解 实例的时候回讲解到时钟使能的方法。 STM32 时钟系统的配置除了初始化的时候在 system_stm32f10x.c 中的 SystemInit()函数中外, 其他的配置主要在 stm32f10x_rcc.c 文件中,里面有很多时钟设置函数,大家可以打开这个文件 浏览一下,基本上看看函数的名称就知道这个函数的作用。在大家设置时钟的时候,一定要仔 细参考 STM32 的时钟图,做到心中有数。这里需要指明一下,对于系统时钟,默认情况下是 在 SystemInit 函数的 SetSysClock()函数中间判断的,而设置是通过宏定义设置的。我们可以看 看 SetSysClock()函数体: static void SetSysClock(void) { #ifdef SYSCLK_FREQ_HSE 90 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 SetSysClockToHSE(); #elif defined SYSCLK_FREQ_24MHz SetSysClockTo24(); #elif defined SYSCLK_FREQ_36MHz SetSysClockTo36(); #elif defined SYSCLK_FREQ_48MHz SetSysClockTo48(); #elif defined SYSCLK_FREQ_56MHz SetSysClockTo56(); #elif defined SYSCLK_FREQ_72MHz SetSysClockTo72(); #endif } 这段代码非常简单,就是判断系统宏定义的时钟是多少,然后设置相应值。我们系统默认宏定 义是 72MHz: #define SYSCLK_FREQ_72MHz 72000000 如果你要设置为 36MHz,只需要注释掉上面代码,然后加入下面代码即可: #define SYSCLK_FREQ_36MHz 36000000 同时还要注意的是,当我们设置好系统时钟后,可以通过变量 SystemCoreClock 获取系统时钟 值,如果系统是 72M 时钟,那么 SystemCoreClock=72000000。这是在 system_stm32f10x.c 文件 中设置的: #ifdef SYSCLK_FREQ_HSE uint32_t SystemCoreClock = SYSCLK_FREQ_HSE; #elif defined SYSCLK_FREQ_36MHz uint32_t SystemCoreClock = SYSCLK_FREQ_36MHz; #elif defined SYSCLK_FREQ_48MHz uint32_t SystemCoreClock = SYSCLK_FREQ_48MHz; #elif defined SYSCLK_FREQ_56MHz uint32_t SystemCoreClock = SYSCLK_FREQ_56MHz; #elif defined SYSCLK_FREQ_72MHz uint32_t SystemCoreClock = SYSCLK_FREQ_72MHz; #else uint32_t SystemCoreClock = HSI_VALUE; #endif 这里总结一下 SystemInit()函数中设置的系统时钟大小: SYSCLK(系统时钟) =72MHz AHB 总线时钟(使用 SYSCLK) =72MHz APB1 总线时钟(PCLK1) =36MHz APB2 总线时钟(PCLK2) =72MHz PLL 时钟 =72MHz 91 4.4 端口复用和重映射 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 4.4.1 端口复用功能 STM32 有很多的内置外设,这些外设的外部引脚都是与 GPIO 复用的。也就是说,一个 GPIO 如果可以复用为内置外设的功能引脚,那么当这个 GPIO 作为内置外设使用的时候,就叫做复用。 这部分知识在《STM32 中文参考手册 V10》的 P109,P116~P121 有详细的讲解哪些 GPIO 管脚是 可以复用为哪些内置外设的。这里我们就不一一讲解。 大家都知道,MCU 都有串口,STM32 有好几个串口。比如说 STM32F103ZET6 有 5 个串口,我 们可以查手册知道,串口 1 的引脚对应的 IO 为 PA9,PA10.PA9,PA10 默认功能是 GPIO,所以当 PA9,PA10 引脚作为串口 1 的 TX,RX 引脚使用的时候,那就是端口复用。 图 4.4.1.1 串口 1 复用管脚 复用端口初始化有几个步骤: 1) GPIO 端口时钟使能。要使用到端口复用,当然要使能端口的时钟了。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 2) 复用的外设时钟使能。比如你要将端口 PA9,PA10 复用为串口,所以要使能串口时钟。 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); 3) 端口模式配置。 在 IO 复用位内置外设功能引脚的时候,必须设置 GPIO 端口的模式,至于 在复用功能下 GPIO 的模式是怎么对应的,这个可以查看手册《STM32 中文参考手册 V10》 P110 的表格“8.1.11 外设的 GPIO 配置”。这里我们拿 Usart1 举例: 图 4.4.1.2 串口复用 GPIO 配置 从表格中可以看出,我们要配置全双工的串口 1,那么 TX 管脚需要配置为推挽复用输出, RX 管脚配置为浮空输入或者带上拉输入。 //USART1_TX PA.9 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART1_RX PA.10 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); 上面代码的含义在我们的第一个实验学完之后大家自然会了解,这里只是做个概括。 所以,我们在使用复用功能的是时候,最少要使能 2 个时钟: 1) GPIO 时钟使能 2) 复用的外设时钟使能 92 同时要初始化 GPIO 以及复用外设功能 4.4.2 端口重映射 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 为了使不同器件封装的外设 IO 功能数量达到最优,可以把一些复用功能重新映射到其他一 些引脚上。STM32 中有很多内置外设的输入输出引脚都具有重映射(remap)的功能。我们知道每 个内置外设都有若干个输入输出引脚,一般这些引脚的输出端口都是固定不变的,为了让设计 工程师可以更好地安排引脚的走向和功能,在 STM32 中引入了外设引脚重映射的概念,即一个 外设的引脚除了具有默认的端口外,还可以通过设置重映射寄存器的方式,把这个外设的引脚 映射到其它的端口。 简单的讲就是把管脚的外设功能映射到另一个管脚,但不是可以随便映射的,具体对应关 系《STM32 中文参考手册 V10》的 P116 页“8.3 复用功能和调试配置”有讲解。这里我们同样 拿串口 1 为例来讲解。 图 4.4.2.1 串口重映射管脚表 上图是截取的中文参考手册中的重映射表,从表中可以看出,默认情况下,串口 1 复用的时候 的引脚位 PA9,PA10,同时我们可以将 TX 和 RX 重新映射到管脚 PB6 和 PB7 上面去。 所以重映射我们同样要使能复用功能的时候讲解的 2 个时钟外,还要使能 AFIO 功能时钟,然后 要调用重映射函数。详细步骤为: 1)使能 GPIOB 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 2)使能串口 1 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); 3)使能 AFIO 时钟: RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); 4)开启重映射: GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); 这样就将串口的 TX 和 RX 重映射到管脚 PB6 和 PB7 上面了。至于有哪些功能可以重映射,大家 除了查看中文参考手册之外,还可以从 GPIO_PinRemapConfig 函数入手查看第一个入口参数的 取值范围可以得知。在 stm32f10x_gpio.h 文件中定义了取值范围为下面宏定义的标识符,这里 我们贴一小部分: #define GPIO_Remap_SPI1 ((uint32_t)0x00000001) 93 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define GPIO_Remap_I2C1 ((uint32_t)0x00000002) #define GPIO_Remap_USART1 ((uint32_t)0x00000004) #define GPIO_Remap_USART2 ((uint32_t)0x00000008) #define GPIO_PartialRemap_USART3 ((uint32_t)0x00140010) #define GPIO_FullRemap_USART3 ((uint32_t)0x00140030) 从上面可以看出,USART1 只有一种重映射,而对于 USART3,存在部分重映射和完全重映射。所 谓部分重映射就是部分管脚和默认的是一样的,而部分管脚是重新映射到其他管脚。而完全重 映射就是所有管脚都重新映射到其他管脚。看看手册中的 USART3 重映射表: 图 4.4.2.2 USART3 重映射管脚对应表 部分重映射就是 PB10,PB11,PB12 重映射到 PC10,PC11,PC12 上。而 PB13 和 PB14 和没有重映射 情况是一样的,都是 USART3_CTS 和 USART3_RTS 对应管脚。完全重映射就是将这两个脚重新映 射到 PD11 和 PD12 上去。我们要使用 USART3 的部分重映射,我们调用函数方法为: GPIO_PinRemapConfig(GPIO_PartialRemap_USART3, ENABLE); 这些知识我们后面在使用的过程中间还会讲解,这里只是对重映射概念做个简要的描述。 4.5 STM32 NVIC 中断优先级管理 这节我们将对 STM32 的重要只是中断管理做个详细的介绍。 CM3 内核支持 256 个中断,其中包含了 16 个内核中断和 240 个外部中断,并且具有 256 级的可编程中断设置。但 STM32 并没有使用 CM3 内核的全部东西,而是只用了它的一部分。 STM32 有 84 个中断,包括 16 个内核中断和 68 个可屏蔽中断,具有 16 级可编程的中断优先级。 而我们常用的就是这 68 个可屏蔽中断,但是 STM32 的 68 个可屏蔽中断,在 STM32F103 系列 上面,又只有 60 个(在 107 系列才有 68 个)。因为我们开发板选择的芯片是 STM32F103 系列 的所以我们就只针对 STM32F103 系列这 60 个可屏蔽中断进行介绍。 在 MDK 内,与 NVIC 相关的寄存器,MDK 为其定义了如下的结构体: typedef struct { vu32 ISER[2]; u32 RESERVED0[30]; 94 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 vu32 ICER[2]; u32 RSERVED1[30]; vu32 ISPR[2]; u32 RESERVED2[30]; vu32 ICPR[2]; u32 RESERVED3[30]; vu32 IABR[2]; u32 RESERVED4[62]; vu32 IPR[15]; } NVIC_TypeDef; STM32 的中断在这些寄存器的控制下有序的执行的。只有了解这些中断寄存器,才能了解 STM32 的中断。下面简要介绍这几个寄存器: ISER[2]:ISER 全称是:Interrupt Set-Enable Registers,这是一个中断使能寄存器组。上面 说了 STM32F103 的可屏蔽中断只有 60 个,这里用了 2 个 32 位的寄存器,总共可以表示 64 个 中断。而 STM32F103 只用了其中的前 60 位。ISER[0]的 bit0~bit31 分别对应中断 0~31。ISER[1] 的 bit0~27 对应中断 32~59;这样总共 60 个中断就分别对应上了。你要使能某个中断,必须设 置相应的 ISER 位为 1,使该中断被使能(这里仅仅是使能,还要配合中断分组、屏蔽、IO 口映 射等设置才算是一个完整的中断设置)。具体每一位对应哪个中断,请参考 stm32f10x_nvic..h 里 面的第 36 行处。 ICER[2]:全称是:Interrupt Clear-Enable Registers,是一个中断除能寄存器组。该寄存器组 与 ISER 的作用恰好相反,是用来清除某个中断的使能的。其对应位的功能,也和 ICER 一样。 这里要专门设置一个 ICER 来清除中断位,而不是向 ISER 写 0 来清除,是因为 NVIC 的这些寄 存器都是写 1 有效的,写 0 是无效的。具体为什么这么设计,请看《CM3 权威指南》第 125 页, NVIC 概览一章。 ISPR[2]:全称是:Interrupt Set-Pending Registers,是一个中断挂起控制寄存器组。每个位 对应的中断和 ISER 是一样的。通过置 1,可以将正在进行的中断挂起,而执行同级或更高级别 的中断。写 0 是无效的。 ICPR[2]:全称是:Interrupt Clear-Pending Registers,是一个中断解挂控制寄存器组。其作 用与 ISPR 相反,对应位也和 ISER 是一样的。通过设置 1,可以将挂起的中断接挂。写 0 无效。 IABR[2]:全称是:Interrupt Active Bit Registers,是一个中断激活标志位寄存器组。这是一 个只读寄存器,通过它可以知道当前在执行的中断是哪一个。在中断执行完了由硬件自动清零。 对应位所代表的中断和 ISER 一样,如果为 1,则表示该位所对应的中断正在被执行。 IPR[15]:全称是:Interrupt Priority Registers,是一个中断优先级控制的寄存器组。这个寄 存器组相当重要!STM32 的中断分组与这个寄存器组密切相关。因为 STM32 的中断多达 60 多 个,所以 STM32 采用中断分组的办法来确定中断的优先级。IPR 寄存器组由 15 个 32bit 的寄存 器组成,每个可屏蔽中断占用 8bit,这样总共可以表示 15*4=60 个可屏蔽中断。刚好和 STM32 的可屏蔽中断数相等。IPR[0]的[31~24],[23~16],[15~8],[7~0]分别对应中中断 3~0,依次类 推,总共对应 60 个外部中断。而每个可屏蔽中断占用的 8bit 并没有全部使用,而是 只用了高 4 位。这 4 位,又分为抢占优先级和子优先级。抢占优先级在前,子优先级在后。而这两个优 先级各占几个位又要根据 SCB->AIRCR 中中断分组的设置来决定。 这里简单介绍一下 STM32 的中断分组:STM32 将中断分为 5 个组,组 0~4。该分组的设 置是由 SCB->AIRCR 寄存器的 bit10~8 来定义的。具体的分配关系如表 4.5.1 所示: 组 AIRCR[10:8] bit[7:4]分配情况 分配结果 95 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 0 111 0:4 0 位抢占优先级,4 位响应优先级 1 110 1:3 1 位抢占优先级,3 位响应优先级 2 101 2:2 2 位抢占优先级,2 位响应优先级 3 100 3:1 3 位抢占优先级,1 位响应优先级 4 011 4:0 4 位抢占优先级,0 位响应优先级 表 4.5.1 AIRCR 中断分组设置表 通过这个表,我们就可以清楚的看到组 0~4 对应的配置关系,例如组设置为 3,那么此时 所有的 60 个中断,每个中断的中断优先寄存器的高四位中的最高 3 位是抢占优先级,低 1 位是 响应优先级。每个中断,你可以设置抢占优先级为 0~7,响应优先级为 1 或 0。抢占优先级的 级别高于响应优先级。而数值越小所代表的优先级就越高。 这里需要注意两点:第一,如果两个中断的抢占优先级和响应优先级都是一样的话,则看 哪个中断先发生就先执行;第二,高优先级的抢占优先级是可以打断正在进行的低抢占优先级 中断的。而抢占优先级相同的中断,高优先级的响应优先级不可以打断低响应优先级的中断。 结合实例说明一下:假定设置中断优先级组为 2,然后设置中断 3(RTC 中断)的抢占优先级 为 2,响应优先级为 1。中断 6(外部中断 0)的抢占优先级为 3,响应优先级为 0。中断 7(外 部中断 1)的抢占优先级为 2,响应优先级为 0。那么这 3 个中断的优先级顺序为:中断 7>中 断 3>中断 6。 上面例子中的中断 3 和中断 7 都可以打断中断 6 的中断。而中断 7 和中断 3 却不可以相互 打断! 通过以上介绍,我们熟悉了 STM32 中断设置的大致过程。接下来我们介绍如何使用库函 数实现以上中断分组设置以及中断优先级管理,使得我们以后的中断设置简单化。NVIC 中断 管理函数主要在 misc.c 文件里面。 首先要讲解的是中断优先级分组函数 NVIC_PriorityGroupConfig,其函数申明如下: void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup); 这个函数的作用是对中断的优先级进行分组,这个函数在系统中只能被调用一次,一旦分 组确定就最好不要更改。这个函数我们可以找到其实现: void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup) { assert_param(IS_NVIC_PRIORITY_GROUP(NVIC_PriorityGroup)); SCB->AIRCR = AIRCR_VECTKEY_MASK | NVIC_PriorityGroup; } 从函数体可以看出,这个函数唯一目的就是通过设置 SCB->AIRCR 寄存器来设置中断优先级分 组,这在前面寄存器讲解的过程中已经讲到。而其入口参数通过双击选中函数体里面的 “IS_NVIC_PRIORITY_GROUP”然后右键“Go to defition of …”可以查看到为: #define IS_NVIC_PRIORITY_GROUP(GROUP) (((GROUP) == NVIC_PriorityGroup_0) || ((GROUP) == NVIC_PriorityGroup_1) || \ ((GROUP) == NVIC_PriorityGroup_2) || \ ((GROUP) == NVIC_PriorityGroup_3) || \ ((GROUP) == NVIC_PriorityGroup_4)) 这也是我们上面表 4.5.1 讲解的,分组范围为 0-4。比如我们设置整个系统的中断优先级分组值 为 2,那么方法是: NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 96 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 这样就确定了一共为“2 位抢占优先级,2 位响应优先级”。 设置好了系统中断分组,那么对于每个中断我们又怎么确定他的抢占优先级和响应优先级 呢?下面我们讲解一个重要的函数为中断初始化函数 NVIC_Init,其函数申明为: void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct) 其中 NVIC_InitTypeDef 是一个结构体,我们可以看看结构体的成员变量: typedef struct { uint8_t NVIC_IRQChannel; uint8_t NVIC_IRQChannelPreemptionPriority; uint8_t NVIC_IRQChannelSubPriority; FunctionalState NVIC_IRQChannelCmd; } NVIC_InitTypeDef; NVIC_InitTypeDef 结构体中间有三个成员变量,这三个成员变量的作用是: NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到 每个中断对应的名字。例如 USART1_IRQn。 NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。 NVIC_IRQChannelSubPriority:定义这个中断的子优先级别。 NVIC_IRQChannelCmd:该中断是否使能。 比如我们要使能串口 1 的中断,同时设置抢占优先级为 1,子优先级位 2,初始化的方法是: USART_InitTypeDef USART_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//串口 1 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1 ;// 抢占优先级为 1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;// 子优先级位 2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //根据上面指定的参数初始化 NVIC 寄存器 这里我们讲解了中断的分组的概念以及设定优先级值的方法,至于每种优先级还有一些关于清 除中断,查看中断状态,这在后面我们讲解每个中断的时候会详细讲解到。最后我们总结一下 中断优先级设置的步骤: 1. 系统运行开始的时候设置中断分组。确定组号,也就是确定抢占优先级和子优先级的 分配位数。调用函数为 NVIC_PriorityGroupConfig(); 2. 设置所用到的中断的中断优先级别。对每个中断调用函数为 NVIC_Init(); 4.6 MDK 中寄存器地址名称映射分析 之所以要讲解这部分知识,是因为经常会遇到客户提到不明白 MDK 中那些结构体是怎么与 寄存器地址对应起来的。这里我们就做一个简要的分析吧。 首先我们看看 51 中是怎么做的。51 单片机开发中经常会引用一个 reg51.h 的头文件,下 面我们看看他是怎么把名字和寄存器联系起来的: sfr P0 =0x80; 97 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 sfr 也是一种扩充数据类型,点用一个内存单元,值域为 0~255。利用它可以访问 51 单片 机内部的所有特殊功能寄存器。如用 sfr P1 = 0x90 这一句定义 P1 为 P1 端口在片内的寄存 器。然后我们往地址为 0x80 的寄存器设值的方法是:P0=value; 那么在 STM32 中,是否也可以这样做呢??答案是肯定的。肯定也可以通过同样的方 式来做,但是 STM32 因为寄存器太多太多,如果一一以这样的方式列出来,那要好大的篇 幅,既不方便开发,也显得太杂乱无序的感觉。所以 MDK 采用的方式是通过结构体来将 寄存器组织在一起。下面我们就讲解 MDK 是怎么把结构体和地址对应起来的,为什么我 们修改结构体成员变量的值就可以达到操作对应寄存器的值。这些事情都是在 stm32f10x.h 文件中完成的。我们通过 GPIOA 的几个寄存器的地址来讲解吧。 首先我们可以查看《STM32 中文参考手册 V10》中的寄存器地址映射表(P159): 图 4.6.1 GPIO 寄存器地址映像 从这个表我们可以看出,GPIOA 的 7 个寄存器都是 32 位的,所以每个寄存器占有 4 个地址,一共占用 28 个地址,地址偏移范围为(000h~01Bh)。这个地址偏移是相对 GPIOA 的基地址而言的。GPIOA 的基地址是怎么算出来的呢?因为 GPIO 都是挂载在 APB2 总线 之上,所以它的基地址是由 APB2 总线的基地址+GPIOA 在 APB2 总线上的偏移地址决定 的。同理依次类推,我们便可以算出 GPIOA 基地址了。这里设计到总线的一些知识,我们 在后面会讲到。下面我们打开 stm32f10x.h 定位到 GPIO_TypeDef 定义处: typedef struct { __IO uint32_t CRL; __IO uint32_t CRH; __IO uint32_t IDR; __IO uint32_t ODR; __IO uint32_t BSRR; __IO uint32_t BRR; __IO uint32_t LCKR; } GPIO_TypeDef; 然后定位到: 98 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE) 可以看出,GPIOA 是将 GPIOA_BASE 强制转换为 GPIO_TypeDef 指针,这句话的意思是, GPIOA 指向地址 GPIOA_BASE,GPIOA_BASE 存放的数据类型为 GPIO_TypeDef。然后双 击“GPIOA_BASE”选中之后右键选中“Go to definition of ”,便可一查看 GPIOA_BASE 的宏定义: #define GPIOA_BASE 依次类推,可以找到最顶层: (APB2PERIPH_BASE + 0x0800) #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) #define PERIPH_BASE ((uint32_t)0x40000000) 所以我们便可以算出 GPIOA 的基地址位: GPIOA_BASE= 0x40000000+0x10000+0x0800=0x40010800 下面我们再跟《STM32 中文参考手册 V10》比较一下看看 GPIOA 的基地址是不是 0x40010800。截图 P28 存储器映射表我们可以看到,GPIOA 的起始地址也就是基地址确实 是 0x40010800: 图 4.6.2 GPIO 存储器地址映射表 同样的道理,我们可以推算出其他外设的基地址。 上面我们已经知道 GPIOA 的基地址,那么那些 GPIOA 的 7 个寄存器的地址又是怎么 算出来的呢??在上面我们讲过 GPIOA 的各个寄存器对于 GPIOA 基地址的偏移地址,所 以我们自然可以算出来每个寄存器的地址。 GPIOA 的寄存器的地址=GPIOA 基地址+寄存器相对 GPIOA 基地址的偏移值 这个偏移值在上面的寄存器地址映像表中可以查到。 那么在结构体里面这些寄存器又是怎么与地址一一对应的呢?这里涉及到结构体成员 变量地址对齐方式方面的知识,这方面的知识大家可以在网上查看相关资料复习一下,这 里我们不做详细讲解。在我们定义好地址对齐方式之后,每个成员变量对应的地址就可以 根据其基地址来计算。对于结构体类型 GPIO_TypeDef,他的所有成员变量都是 32 位,成 员变量地址具有连续性。所以自然而然我们就可以算出 GPIOA 指向的结构体成员变量对应 地址了。 寄存器 偏移地址 实际地址=基地址+偏移地址 GPIOA->CRL 0x00 0x40010800+0x00 GPIOA->CRH; 0x04 0x40010800+0x04 GPIOA->IDR; 0x08 0x40010800+0x08 GPIOA->ODR 0x0c 0x40010800+0x0c GPIOA->BSRR 0x10 0x40010800+0x10 GPIOA->BRR 0x14 0x40010800+0x14 GPIOA->LCKR 0x18 0x40010800+0x18 表 4.6.3 GPIOA 各寄存器实际地址表 99 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 我们可以把 GPIO_TypeDef 的定义中的成员变量的顺序和 GPIOx 寄存器地址映像对比 可以发现,他们的顺序是一致的,如果不一致,就会导致地址混乱了。 这就是为什么固件库里面:GPIOA->BRR=value;就是设置地址为 0x40010800 +0x014(BRR 偏移量)=0x40010814 的寄存器 BRR 的值了。它和 51 里面 P0=value 是设置地 址为 0x80 的 P0 寄存器的值是一样的道理。 看到这里你是否会学起来踏实一点呢??STM32 使用的方式虽然跟 51 单片机不一样, 但是原理都是一致的。 4.7 MDK 固件库快速组织代码技巧 这一节主要讲解在使用 MDK 固件库开发的时候的一些小技巧,仅供初学者参考。这节的 知识大家可以在学习第一个跑马灯实验的时候参考一下,对初学者应该很有帮助。我们就用最 简单的 GPIO 初始化函数为例。 现在我们要初始化某个 GPIO 端口,我们要怎样快速操作呢?在头文件 stm32f10x_gpio.h 头文件中,定义 GPIO 初始化函数为: void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); 现在我们想写初始化函数,那么我们在不参考其他代码的前提下,怎么组织代码呢? 首先,我们可以看出,函数的入口参数是 GPIO_TypeDef 类型指针和 GPIO_InitTypeDef 类 型 指 针 , 因 为 GPIO_TypeDef 入 口 参 数 比 较 简 单 , 所 以 我 们 通 过 第 二 个 入 口 参 数 GPIO_InitTypeDef 类型指针来讲解。双击 GPIO_InitTypeDef 后右键选择“Go to definition…”,如 下图 4.7.1: 图 4.7.1 查看类型定义方法 于是定位到 stm32f10x_gpio.h 中 GPIO_InitTypeDef 的定义处: typedef struct { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; 100 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIOMode_TypeDef GPIO_Mode; GPIOMode_TypeDef */ }GPIO_InitTypeDef; 可以看到这个结构体有 3 个成员变量,这也告诉我们一个信息,一个 GPIO 口的状态是由速度 (Speed)和模式(Mode)来决定的。我们首先要定义一个结构体变量,下面我们定义: GPIO_InitTypeDef GPIO_InitStructure; 接着我们要初始化结构体变量 GPIO_InitStructure。首先我们要初始化成员变量 GPIO_Pin,这个 时候我们就有点迷糊了,这个变量到底可以设置哪些值呢?这些值的范围有什么规定吗? 这里我们就要找到 GPIO_Init()函数的实现处,同样,双击 GPIO_Init,右键点击“Go to definition of …”,这样光标定位到 stm32f10x_gpio.c 文件中的 GPIO_Init 函数体开始处,我们可以 看到在函数的开始处有如下几行: void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct) { …… /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); assert_param(IS_GPIO_MODE(GPIO_InitStruct->GPIO_Mode)); assert_param(IS_GPIO_PIN(GPIO_InitStruct->GPIO_Pin)); …… assert_param(IS_GPIO_SPEED(GPIO_InitStruct->GPIO_Speed)); …… } 顾名思义,assert_param 函数式对入口参数的有效性进行判断,所以我们可以从这个函数入手, 确 定 我 们 的 入 口 参 数 的 范 围 。 第 一 行 是 对 第 一 个 参 数 GPIOx 进 行 有 效 性 判 断 , 双 击 “IS_GPIO_ALL_PERIPH”右键点击“go to defition of…” 定位到了下面的定义: #define IS_GPIO_ALL_PERIPH(PERIPH) (((PERIPH) == GPIOA) || \ ((PERIPH) == GPIOB) || \ ((PERIPH) == GPIOC) || \ ((PERIPH) == GPIOD) || \ ((PERIPH) == GPIOE) || \ ((PERIPH) == GPIOF) || \ ((PERIPH) == GPIOG)) 很明显可以看出,GPIOx 的取值规定只允许是 GPIOA~GPIOG。 同样的办法,我们双击“IS_GPIO_MODE” 右键点击“go to defition of…”,定位到下面的定义: typedef enum { GPIO_Mode_AIN = 0x0, GPIO_Mode_IN_FLOATING = 0x04, GPIO_Mode_IPD = 0x28, GPIO_Mode_IPU = 0x48, GPIO_Mode_Out_OD = 0x14, GPIO_Mode_Out_PP = 0x10, GPIO_Mode_AF_OD = 0x1C, 101 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_Mode_AF_PP = 0x18 }GPIOMode_TypeDef; #define IS_GPIO_MODE(MODE) (((MODE) == GPIO_Mode_AIN) || \ ((MODE) == GPIO_Mode_IN_FLOATING) || \ ((MODE) == GPIO_Mode_IPD) || \ ((MODE) == GPIO_Mode_IPU) || \ ((MODE) == GPIO_Mode_Out_OD) || \ ((MODE) == GPIO_Mode_Out_PP) || \ ((MODE) == GPIO_Mode_AF_OD) || \ ((MODE) == GPIO_Mode_AF_PP)) 所以 GPIO_InitStruct->GPIO_Mode 成员的取值范围只能是上面定义的 8 种。这 8 中模式是通过 一个枚举类型组织在一起的。 同样的方法可以找出 GPIO_Speed 的参数限制: typedef enum { GPIO_Speed_10MHz = 1, GPIO_Speed_2MHz, GPIO_Speed_50MHz }GPIOSpeed_TypeDef; #define IS_GPIO_SPEED(SPEED) (((SPEED) == GPIO_Speed_10MHz) || \ ((SPEED) == GPIO_Speed_2MHz) || \ ((SPEED) == GPIO_Speed_50MHz)) 同样的方法我们双击“IS_GPIO_PIN” 右键点击“go to defition of…”,定位到下面的定义: #define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != (uint16_t)0x00)) 可以看出,GPIO_Pin 成员变量的取值范围为 0x0000 到 0xffff,那么是不是我们写代码初始化就 是直接给一个 16 位的数字呢?这也是可以的,但是大多数情况下,MDK 不会让你直接在入口 参数处设置一个简单的数字,因为这样代码的可读性太差,MDK 会将这些数字的意思通过宏 定义定义出来,这样可读性大大增强。我们可以看到在 IS_GPIO_PIN(PIN)宏定义的上面还有数 行宏定义: #define GPIO_Pin_0 #define GPIO_Pin_1 #define GPIO_Pin_2 #define GPIO_Pin_3 #define GPIO_Pin_4 …… #define GPIO_Pin_14 #define GPIO_Pin_15 #define GPIO_Pin_All ((uint16_t)0x0001) ((uint16_t)0x0002) ((uint16_t)0x0004) ((uint16_t)0x0008) ((uint16_t)0x0010) /*!< Pin 0 selected */ /*!< Pin 1 selected */ /*!< Pin 2 selected */ /*!< Pin 3 selected */ /*!< Pin 4 selected */ ((uint16_t)0x4000) /*!< Pin 14 selected */ ((uint16_t)0x8000) /*!< Pin 15 selected */ ((uint16_t)0xFFFF) /*!< All pins selected */ #define IS_GPIO_PIN(PIN) ((((PIN) & (uint16_t)0x00) == 0x00) && ((PIN) != 102 (uint16_t)0x00)) STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 这些宏定义 GPIO_Pin_0~GPIO_Pin_ All 就是 MDK 事先定义好的,我们写代码的时候初始化 GPIO_Pin 的时候入口参数可以是这些宏定义。对于这种情况,MDK 一般把取值范围的宏定义 放在判断有效性语句的上方,这样是为了方便大家查找。 讲到这里,我们基本对 GPIO_Init 的入口参数有比较详细的了解了。于是我们可以组织起 来下面的代码: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; // GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); 接着又有一个问题会被提出来,这个初始化函数一次只能初始化一个 IO 口吗?我要同时 初始化很多个 IO 口,是不是要复制很多次这样的初始化代码呢? 这里又有一个小技巧了。从上面的 GPIO_Pin_x 的宏定义我们可以看出,这些值是 0,1,2,4 这样的数字,所以每个 IO 口选定都是对应着一个位,16 位的数据一共对应 16 个 IO 口。这个 位为 0 那么这个对应的 IO 口不选定,这个位为 1 对应的 IO 口选定。如果多个 IO 口,他们都 是对应同一个 GPIOx,那么我们可以通过|(或)的方式同时初始化多个 IO 口。这样操作的前 提是,他们的 Mode 和 Speed 参数相同,因为 Mode 和 Speed 参数并不能一次定义多种。所以 初始化多个 IO 口的方式可以是如下: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5| GPIO_Pin_6| GPIO_Pin_7; //指定端口 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //端口模式:推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度 GPIO_Init(GPIOB, &GPIO_InitStructure); //初始化 对于那些参数可以通过|(或)的方式连接,这既有章可循,同时也靠大家在开发过程中不断积累。 有客户经常问到,我每次使能时钟的时候都要去查看时钟树看那些外设是挂载在那个总线 之下的,这好麻烦。学到这里我相信大家就可以很快速的解决这个问题了。 在 stm32f10x.h 文件里面我们可以看到如下的宏定义: #define RCC_APB2Periph_GPIOA ((uint32_t)0x00000004) #define RCC_APB2Periph_GPIOB ((uint32_t)0x00000008) #define RCC_APB2Periph_GPIOC ((uint32_t)0x00000010) #define RCC_APB1Periph_TIM2 #define RCC_APB1Periph_TIM3 #define RCC_APB1Periph_TIM4 ((uint32_t)0x00000001) ((uint32_t)0x00000002) ((uint32_t)0x00000004) #define RCC_AHBPeriph_DMA1 ((uint32_t)0x00000001) #define RCC_AHBPeriph_DMA2 ((uint32_t)0x00000002) 从上图可以很明显的看出 GPIOA~GPIOC 是挂载在 APB2 下面,TIM2~TIM4 是挂载在 APB1 下 面 , DMA 是 挂 载 在 AHB 下 面 。 所 以 在 使 能 DMA 的 时 候 记 住 要 调 用 的 是 103 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 RCC_AHBPeriphClock()函数使能,在使能 GPIO 的时候调用的是 RCC_APB2PeriphResetCmd() 函数使能。 大家会觉得上面讲解有点麻烦,每次要去查找 assert_param()这个函数去寻找,那么有没有 更好的办法呢?大家可以打开 GPIO_InitTypeDef 结构体定义: typedef struct { uint16_t GPIO_Pin; /*!< Specifies the GPIO pins to be configured. This parameter can be any value of @ref GPIO_pins_define */ GPIOSpeed_TypeDef GPIO_Speed; /*!< Specifies the speed for the selected pins. This parameter can be a value of @ref GPIOSpeed_TypeDef */ GPIOMode_TypeDef GPIO_Mode; /*!< Specifies the operating mode for the selected pins.This parameter can be a value of @ref GPIOMode_TypeDef */ }GPIO_InitTypeDef; 从上图的结构体成员后面的注释我们可以看出 GPIO_Mode 的意思是 “Specifies the operating mode for the selected pins.This parameter can be a value of @ref GPIOMode_TypeDef”。从这段注释可以看出 GPIO_Mode 的取值为 GPIOMode_TypeDef 枚举类 型的枚举值,大家同样可以用之前讲解的方法右键双击“GPIOMode_TypeDef”选择“Go to definition of …”即可查看其取值范围。如果要确定详细的信息呢我们就得去查看手册了。对于 去查看手册的哪个地方,你可以在函数 GPIO_Init()的函数体中搜索 GPIO_Mode 关键字,然后 查看库函数设置 GPIO_Mode 是设置的哪个寄存器的哪个位,然后去中文参考手册查看该寄存 器相应位的定义以及前后文的描述。 这一节我们就讲解到这里,希望能对大家的开发有帮助。 104 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第五章 SYSTEM 文件夹介绍 前面章节,我们介绍了如何在 MDK5 下建立 STM32 工程,在这个新建的工程之中, 我们用到了一个 SYSTEM 文件夹里面的代码,此文件夹里面的代码由 ALIENTEK 提供, 包含了几乎每个实验都可能用到的延时函数,位带操作,串口打印代码等。这里我们组织 在 SYSTEM 文件夹下面,目的也就是让这些常用的代码能随用随调。 SYSTEM 文件夹下包含了 delay、sys、usart 等三个文件夹。分别包含了 delay.c、sys.c、 usart.c 及其头文件 delay.h,sys.h,usart.h。 本章,我们将向大家介绍这些代码,通过这章的学习,大家将了解到这些代码的由来, 也希望大家可以灵活使用 SYSTEM 文件夹提供的函数,实际应用到自己的项目中去。 本章包括如下 3 个小结: 5.1,delay 文件夹代码介绍; 5.2,sys 文件夹代码介绍; 5.3,usart 文件夹代码介绍; 5.1 delay 文件夹代码介绍 delay 文件夹内包含了 delay.c 和 delay.h 两个文件,这两个文件用来实现系统的延时功 能,其中包含 3 个函数(这里我们不讲 SysTick_Handler 函数,该函数在讲 ucos 的时候再 介绍): void delay_init(); void delay_ms(u16 nms); void delay_us(u32 nus); 下面分别介绍这三个函数,在介绍之前,我们先了解一下编程思想:CM3 内核的处理 器,内部包含了一个 SysTick 定时器,SysTick 是一个 24 位的倒计数定时器,当计到 0 时, 将从 RELOAD 寄存器中自动重装载定时初值。只要不把它在 SysTick 控制及状态寄存器 中的使能位清除,就永不停息。SysTick 在《STM32 的参考手册》(这里是指 V10.0 版本, 下同)里面介绍的很简单,其详细介绍,请参阅《Cortex-M3 权威指南》第 133 页。我们 就是利用 STM32 的内部 SysTick 来实现延时的,这样既不占用中断,也不占用系统定时器。 这里我们将介绍的是 ALIENTEK 提供的最新版本的延时函数,该版本的延时函数支持 在 ucos 下面使用,它可以和 ucos 共用 systick 定时器。首先我们简单介绍下 ucos 的时钟: ucos 运 行 需 要 一 个 系 统 时 钟 节 拍 ( 类 似 “ 心 跳 ”), 而 这 个 节 拍 是 固 定 的 ( 由 OS_TICKS_PER_SEC 设置),比如 5ms(设置:OS_TICKS_PER_SEC=200 即可),在 STM32 下面,一般是由 systick 来提供这个节拍,也就是 systick 要设置为 5ms 中断一次,为 ucos 提供时钟节拍,而且这个时钟一般是不能被打断的(否则就不准了)。 因为在 ucos 下 systick 不能再被随意更改,如果我们还想利用 systick 来做 delay_us 或 者 delay_ms 的延时,就必须想点办法了,这里我们利用的是时钟摘取法。以 delay_us 为例, 比如 delay_us(50),在刚进入 delay_us 的时候先计算好这段延时需要等待的 systick 计数 次数,这里为 50*9(假设系统时钟为 72Mhz,那么 systick 每增加 1,就是 1/9us),然后我 们就一直统计 systick 的计数变化,直到这个值变化了 50*9,一旦检测到变化达到或者超 过这个值,就说明延时 50us 时间到了。 下面我们开始介绍这几个函数。 105 5.1.1 delay_init 函数 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 该函数用来初始化 2 个重要参数:fac_us 以及 fac_ms;同时把 SysTick 的时钟源选择 为外部时钟,如果使用了 ucos,那么还会根据 OS_TICKS_PER_SEC 的配置情况,来配置 SysTick 的中断时间,并开启 SysTick 中断。具体代码如下: //初始化延迟函数 //SYSTICK 的时钟固定为 HCLK 时钟的 1/8 void delay_init() { //如果 OS_CRITICAL_METHOD 定义了,说明使用 ucosII 了. #ifdef OS_CRITICAL_METHOD u32 reload; #endif SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);//选择外部时钟 HCLK/8 fac_us=SystemCoreClock/8000000; //为系统时钟的 1/8 //如果 OS_CRITICAL_METHOD 定义了,说明使用 ucosII 了. #ifdef OS_CRITICAL_METHOD reload=SystemCoreClock/8000000; //每秒钟的计数次数 单位为 K reload*=1000000/OS_TICKS_PER_SEC;//根据 OS_TICKS_PER_SEC 设定溢出时间 //reload 为 24 位寄存器,最大值:16777216,在 72M 下, //约 1.86s 左右 fac_ms=1000/OS_TICKS_PER_SEC;//代表 ucos 可以延时的最少单位 SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //开启 SYSTICK 中断 SysTick->LOAD=reload; //每 1/OS_TICKS_PER_SEC 秒中断一次 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; //开启 SYSTICK #else fac_ms=(u16)fac_us*1000;//非 ucos 下,代表每个 ms 需要的 systick 时钟数 #endif } 可以看到,delay_init 函数使用了条件编译,来选择不同的初始化过程,如果不使用 ucos 的时候,就和《不完全手册》介绍的方法是一样的,而如果使用 ucos 的时候,则会进行一 些不同的配置,这里的条件编译是根据 OS_CRITICAL_METHOD 这个宏来确定的,因为 只要使用了 ucos,就一定会定义 OS_CRITICAL_METHOD 这个宏。 SysTick 是 MDK 定义了的一个结构体(在 stm32f10x_map.h 里面),里面包含 CTRL、 LOAD、VAL、CALIB 等 4 个寄存器, SysTick->CTRL 的各位定义如图 5.1.1.1 所示: 106 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 5.1.1.1 SysTick->CTRL 寄存器各位定义 SysTick-> LOAD 的定义如图 5.1.1.2 所示: 图 5.1.1.2 SysTick->LOAD 寄存器各位定义 SysTick-> VAL 的定义如图 5.1.1.3 所示: 图 5.1.1.3 SysTick->VAL 寄存器各位定义 SysTick-> CALIB 不常用,在这里我们也用不到,故不介绍了。 SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);这一句把 SysTick 的时钟 选择外部时钟,这里需要注意的是:SysTick 的时钟源自 HCLK 的 8 分频,假设我们外部 晶振为 8M,然后倍频到 72M,那么 SysTick 的时钟即为 9Mhz,也就是 SysTick 的计数器 VAL 每减 1,就代表时间过了 1/9us。所以 fac_us=SystemCoreClock/8000000;这句话就是计 算在 SystemCoreClock 时钟频率下延时 1us 需要多少个 SysTick 时钟周期。同理, fac_ms=(u16)fac_us*1000;就是计算延时 1ms 需要多少个 SysTick 时钟周期,它自然是 1us 的 1000 倍。初始化将计算出 fac_us 和 fac_ms 的值。 在不使用 ucos 的时候:fac_us,为 us 延时的基数,也就是延时 1us,SysTick->LOAD 所应设置的值。fac_ms 为 ms 延时的基数,也就是延时 1ms,SysTick->LOAD 所应设置的 值。fac_us 为 8 位整形数据,fac_ms 为 16 位整形数据。正因为如此,系统时钟如果不是 8 的倍数,则会导致延时函数不准确,这也是我们推荐外部时钟选择 8M 的原因。这点大家 要特别留意。 当 使 用 ucos 的 时 候 , fac_us , 还 是 us 延 时 的 基 数 , 不 过 这 个 值 不 会 被 写 到 SysTick->LOAD 寄存器来实现延时,而是通过时钟摘取的办法实现的(后面会介绍)。而 fac_ms 则 代 表 ucos 自 带 的 延 时 函 数 所 能 实 现 的 最 小 延 时 时 间 ( 如 OS_TICKS_PER_SEC=200,那么 fac_ms 就是 5ms)。 5.1.2 delay_us 函数 该函数用来延时指定的 us,其参数 nus 为要延时的微秒数。该函数有使用 ucos 和不使 用 ucos 两个版本,这里我们分别介绍,首先是不使用 ucos 的时候,实现函数如下: 107 STM32 不完全手册(库函数版) //延时 nus //nus 为要延时的 us 数. ALIENTEK MiniSTM32 V3.0 开发板教程 void delay_us(u32 nus) { u32 temp; SysTick->LOAD=nus*fac_us; //时间加载 SysTick->VAL=0x00; //清空计数器 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数 do { temp=SysTick->CTRL; } while(temp&0x01&&!(temp&(1<<16)));//等待时间到达 SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; SysTick->VAL =0X00; //清空计数器 //关闭计数器 } 有了上面对 SysTick 寄存器的描述,这段代码不难理解。其实就是先把要延时的 us 数 换算成 SysTick 的时钟数,然后写入 LOAD 寄存器。然后清空当前寄存器 VAL 的内容,再 开启倒数功能。等到倒数结束,即延时了 nus。最后关闭 SysTick,清空 VAL 的值。实现 一次延时 nus 的操作,但是这里要注意 nus 的值,不能太大,必须保证 nus<=(2^24)/fac_us, 否则将导致延时时间不准确。这里特别说明一下:temp&0x01,这一句是用来判断 systick 定时器是否还处于开启状态,可以防止 systick 被意外关闭导致的死循环。这里面有一行开 启 Systick 开始倒数代码需要解释一下: SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; 其中 SysTick_CTRL_ENABLE_Msk 是 MDK 宏定义的一个变量,它的值就是 0x01,这行代 码的意思就是设置 SysTick->CTRL 的第一位为 1,使能定时器。 再来看看使用 ucos 的时候,delay_us 的实现函数如下: //延时 nus //nus 为要延时的 us 数. void delay_us(u32 nus) { u32 ticks; u32 told,tnow,tcnt=0; u32 reload=SysTick->LOAD; ticks=nus*fac_us; //LOAD 的值 //需要的节拍数 tcnt=0; OSSchedLock(); told=SysTick->VAL; //阻止 ucos 调度,防止打断 us 延时 //刚进入时的计数器值 while(1) { tnow=SysTick->VAL; if(tnow!=told) { 108 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 if(tnow=ticks)break;//时间超过/等于要延迟的时间,则退出. } }; OSSchedUnlock(); //开启 ucos 调度 } 这里就正是利用了我们前面提到的时钟摘取法,ticks 是延时 nus 需要等待的 SysTick 计数次数(也就是延时时间),told 用于记录最近一次的 SysTick->VAL 值,然后 tnow 则是 当前的 SysTick->VAL 值,通过他们的对比累加,实现 SysTick 计数次数的统计,统计值存 放在 tcnt 里面,然后哦通过对比 tcnt 和 ticks,来判断延时是否到达,从而达到不修改 SysTick 实现 nus 的延时,从而可以和 ucos 共用一个 SysTick。 上面的 OSSchedLock 和 OSSchedUnlock 是 ucos 提供的两个函数,用于调度上锁和解 锁,这里为了防止 ucos 在 delay_us 的时候打断延时,可能导致的延时不准,所以我们利用 这两个函数来实现免打断,从而保证延时精度!同时,此时的 delay_us,可以实现最长 2^32us 的延时,大概是 4294 秒。 5.1.3 delay_ms 函数 该函数用来延时指定的 ms,其参数 nms 为要延时的微秒数。该函数同样有使用 ucos 和不使用 ucos 两个版本,这里我们分别介绍,首先是不使用 ucos 的时候,实现函数如下: //延时 nms //注意 nms 的范围 //SysTick->LOAD 为 24 位寄存器,所以,最大延时为: //nms<=0xffffff*8*1000/SYSCLK //SYSCLK 单位为 Hz,nms 单位为 ms //对 72M 条件下,nms<=1864 void delay_ms(u16 nms) { u32 temp; SysTick->LOAD=(u32)nms*fac_ms;//时间加载(SysTick->LOAD 为 24bit) SysTick->VAL =0x00; //清空计数器 SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ; //开始倒数 do { temp=SysTick->CTRL; } while(temp&0x01&&!(temp&(1<<16)));//等待时间到达 SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk; //关闭计数器 SysTick->VAL =0X00; //清空计数器 } 此部分代码和 5.1.2 节的 delay_us(非 ucos 版本)大致一样,但是要注意因为 LOAD 仅仅是一个 24bit 的寄存器,延时的 ms 数不能太长。否则超出了 LOAD 的范围,高位会被 109 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 舍去,导致延时不准。最大延迟 ms 数可以通过公式:nms<=0xffffff*8*1000/SYSCLK 计算。 SYSCLK 单位为 Hz,nms 的单位为 ms。如果时钟为 72M,那么 nms 的最大值为 1864ms。 超过这个值,建议通过多次调用 delay_ms 实现,否则就会导致延时不准确。 再来看看使用 ucos 的时候,delay_ms 的实现函数如下: //延时 nms //nms:要延时的 ms 数 void delay_ms(u16 nms) { if(OSRunning==TRUE)//如果 os 已经在跑了 { if(nms>=fac_ms)//延时的时间大于 ucos 的最少时间周期 { OSTimeDly(nms/fac_ms);//ucos 延时 } nms%=fac_ms;//ucos 已经无法提供这么小的延时了,采用普通方式延时 } delay_us((u32)(nms*1000)); //普通方式延时 } 该函数中,OSRunning 是 ucos 正在运行的一个标志,OSTimeDly 是 ucos 提供的一个 基 于 ucos 时 钟 节 拍 的 延 时 函 数 , 其 参 数 代 表 延 时 的 时 钟 节 拍 数 ( 假 设 OS_TICKS_PER_SEC=200,那么 OSTimeDly(1),就代表延时 5ms)。 当 ucos 还未运行的时候,我们的 delay_ms 就是直接由 delay_us 实现的,ucos 下的 delay_us 可以实现很长的延时而不溢出!,所以放心的使用 delay_us 来实现 delay_ms,不过 由于 delay_us 的时候,任务调度被上锁了,所以还是建议不要用 delay_us 来延时很长的时 间,否则影响整个系统的性能。 当 ucos 运行的时候,我们的 delay_ms 函数将先判断延时时长是否大于等于 1 个 ucos 时钟节拍(fac_ms),当大于这个值的时候,我们就通过调用 ucos 的延时函数来实现(此时 任务可以调度),不足 1 个时钟节拍的时候,直接调用 delay_us 函数实现(此时任务无法 调度)。 5.2 sys 文件夹代码介绍 sys 文件夹内包含了 sys.c 和 sys.h 两个文件。在 sys.h 里面定义了 STM32 的 IO 口输入 读取宏定义和输出宏定义。sys.c 里面只定义了一个中断分组函数。下面我们将分别向大家 介绍。 5.2.1 IO 口的位操作实现 该部分代码在 sys.h 文件中,实现对 STM32 各个 IO 口的位操作,包括读入和输出。 当然在这些函数调用之前,必须先进行 IO 口时钟的使能和 IO 口功能定义。此部分仅仅对 IO 口进行输入输出读取和控制。 位带操作简单的说,就是把每个比特膨胀为一个 32 位的字,当访问这些字的时候就达 到了访问比特的目的,比如说 BSRR 寄存器有 32 个位,那么可以映射到 32 个地址上,我 们去访问这 32 个地址就达到访问 32 个比特的目的。这样我们往某个地址写 1 就达到往对 110 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 应比特位写 1 的目的,同样往某个地址写 0 就达到往对应的比特位写 0 的目的。 寄存器32个位 32个对应地址 Bit31 Bit30 Bit29 Address31 Address30 Address29 Bit2 Bit1 Bit0 Address2 Address1 Address 0 图 5.2.2.1 位带映射图 对于上图,我们往 Address0 地址写入 1,那么就可以达到往寄存器的第 0 位 Bit0 赋值 1 的目的。这里我们不想讲得过于复杂,因为位带操作在实际开发中可能只是用来 IO 口的 输入输出还比较方便,其他操作在日常开发中也基本很少用。下面我们看看 sys.h 中位带 操作的定义。 代码如下: #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+(( addr &0xFFFFF)<<5)+(bitnum<<2)) #define MEM_ADDR(addr) *((volatile unsigned long *)(addr)) #define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum)) //IO 口地址映射 #define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C #define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C #define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C #define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C #define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C #define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C #define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C #define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808 #define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08 #define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008 #define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408 #define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808 #define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08 #define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08 //IO 口操作,只对单一的 IO 口! //确保 n 的值小于 16! #define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出 #define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入 #define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出 111 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入 #define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出 #define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入 #define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出 #define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入 #define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出 #define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入 #define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出 #define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入 #define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出 #define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入 以上代码的便是 GPIO 位带操作的具体实现,位带操作的详细说明,在权威指南中有 详细讲解,请参考<>第五章(87 页~92 页)。比如说,我们调用 PAout(1)=1 是设置了 GPIOA 的第一个管脚 GPIOA.1 为 1,实际是设置了寄存器的某个位,但是我们 的定义中可以跟踪过去看到却是通过计算访问了一个地址。上面一系列公式也就是计算 GPIO 的某个 io 口对应的位带区的地址了。 有了上面的代码,我们就可以像 51/AVR 一样操作 STM32 的 IO 口了。比如,我要 PORTA 的第七个 IO 口输出 1,则可以使用 PAout(6)=1;即可实现。我要判断 PORTA 的第 15 个位是否等于 1,则可以使用 if(PAin(14)==1)…;就可以了。 这里顺便说一下,在 sys.h 中的还有个全局宏定义: //0,不支持 ucos //1,支持 ucos #define SYSTEM_SUPPORT_UCOS 0 //定义系统文件夹是否支持 UCOS SYSTEM_SUPPORT_UCOS,这个宏定义用来定义 SYSTEM 文件夹是否支持 ucos,如 果在 ucos 下面使用 SYSTEM 文件夹,那么设置这个值为 1 即可,否则设置为 0(默认)。 5.2.2 中断分组设置函数 在 sys.c 里面只有一个函数就是 void NVIC_Configuration()中断配置函数,在这个函数 里面我们只调用了固件库的中断分组配置函数,这只整个系统的中断分组为组别 2.这个函 数在系统初始化的时候调用即可,并且永远只需要调用一次。 void NVIC_Configuration(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置 NVIC 中断分组 2:2 位抢占//优先级,2 位响应优先级 }} 5.3 usart 文件夹介绍 usart 文件夹内包含了 usart.c 和 usart.h 两个文件。这两个文件用于串口的初始化和中 断接收。这里只是针对串口 1,比如你要用串口 2 或者其他的串口,只要对代码稍作修改 就可以了。usart.c 里面包含了 2 个函数一个是 void USART1_IRQHandler(void);另外一个是 void uart_init(u32 bound);里面还有一段对串口 printf 的支持代码,如果去掉,则会导致 printf 无法使用,虽然软件编译不会报错,但是硬件上 STM32 是无法启动的,这段代码不要去 112 修改。 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 5.3.1 printf 函数支持 这段引入 printf 函数支持的代码在 usart.h 头文件的最上方,这段代码加入之后便可以 通过 printf 函数向串口发送我们需要的内容,方便开发过程中查看代码执行情况以及一些 变量值。这段代码不需要修改,引入到 usart.h 即可。 这段代码为: //加入以下代码,支持 printf 函数,而不需要选择 use MicroLIB #if 1 #pragma import(__use_no_semihosting) //标准库需要的支持函数 struct __FILE { int handle; }; FILE __stdout; //定义_sys_exit()以避免使用半主机模式 _sys_exit(int x) { x = x; } //重定义 fputc 函数 int fputc(int ch, FILE *f) { while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET); USART_SendData(USART1,(uint8_t)ch); return ch; } #endif 5.3.2 uart_init 函数 void uart_init(u32 pclk2,u32 bound)函数是串口 1 初始化函数。该函数有 1 个参数为波 特率,波特率这个参数对于大家来说应该不陌生,这里就不多说了。 void uart_init(u32 bound){ //GPIO 端口设置 GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA |RCC_APB2Periph_AFIO, ENABLE); //使能 USART1,GPIOA 时钟 113 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 //以及复用功能时钟 //USART1_TX PA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.0 发送端 //USART1_RX PA.10 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.10 接收端 //Usart1 NVIC 中断配置 配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //对应中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级 3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //中断优先级配置 //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//波特率设置; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl= USART_HardwareFlowControl_None;//无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx |USART_Mode_Tx;//收发模式 USART_Init(USART1, &USART_InitStructure); //初始化串口 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断 USART_Cmd(USART1, ENABLE); //使能串口 } 下面我们一一分析一下这段初始化代码。首先是一行时钟使能代码: RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1| RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟 这个时钟使能我们在端口复用的时候已经讲解过,大家可以翻到端口复用那一章节,有详细的 讲解。在使用一个内置外设的时候,我们首先要使能相应的 GPIO 时钟,然后使能复用功能时 钟和内置外设时钟。 接下来我们要初始化相应的 GPIO 端口为特定的状态,我们在复用内置外设的时候到底 GPIO 要设置成什么模式呢?这个在我们的端口复用一节也有讲解,那就是在《STM32 中文参 考手册 V10》的 P110“8.1.11 外设的 GPIO 配置”中有讲解,我们就继续截图下来: 114 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 所以接下来的两段代码就是将 TX(PA9)设置为推挽复用输出模式,将 RX(PA10)设置为浮空输入 模式: //USART1_TX PA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9 复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_Init(GPIOA, &GPIO_InitStructure); //USART1_RX PA.10 浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); 对于 GPIO 的知识我们在跑马灯实例会讲解到,这里暂时不做深入的讲解。 紧接着,我们要进行 usart1 的中断初始化,设置抢占优先级值和子优先级的值: //Usart1 NVIC 中断配置 配置 NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级 3 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级 3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道使能 NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化 VIC 寄存器 这段代码在我们的中断管理函数章节 4.5 有讲解中断管理相关的知识,大家可以翻阅一下。 在设置完中断优先级之后,接下来我们要设置串口 1 的初始化参数: //USART 初始化设置 USART_InitStructure.USART_BaudRate = bound;//一般设置为 9600; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl= USART_HardwareFlowControl_None;//无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发 USART_Init(USART1, &USART_InitStructure); //初始化串口 从上面的源码我们可以看出,串口的初始化是通过调用 USART_Init()函数实现,而这个函 数重要的参数就是就是结构体指针变量 USART_InitStructure,下面我们看看结构体定义: typedef struct { uint32_t USART_BaudRate; uint16_t USART_WordLength; uint16_t USART_StopBits; 115 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 uint16_t USART_Parity; uint16_t USART_Mode; uint16_t USART_HardwareFlowControl; } USART_InitTypeDef; 这个结构体有 6 个成员变量,所以我们有 6 个参数需要初始化。 第一个参数 USART_BaudRate 为串口波特率,波特率可以说是串口最重要的参数了,我们 这里通过初始化传入参数 baund 来设定。第二个参数 USART_WordLength 为字长,这里我们设 置为 8 位字长数据格式。第三个参数 USART_StopBits 为停止位设置,我们设置为 1 位停止位。 第四个参数 USART_Parity 设定是否需要奇偶校验,我们设定为无奇偶校验位。第五个参数 USART_Mode 为串口模式,我们设置为全双工收发模式。第六个参数为是否支持硬件流控制, 我们设置为无硬件流控制。 在设置完成串口中断优先级以及串口初始化之后,接下来就是开启串口中断以及使能串口 了: USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断 USART_Cmd(USART1, ENABLE); //使能串口 在开启串口中断和使能串口之后接下来就是写中断处理函数了,下面一节我们将着重讲解 中断处理函数。 5.3.3 USART1_IRQHandler 函数 void USART1_IRQHandler(void)函数是串口 1 的中断响应函数,当串口 1 发生了相应 的中断后,就会跳到该函数执行。中断相应函数的名字是不能随便定义的,一般我们都遵 循 MDK 定义的函数名。这些函数名字在启动文件 startup_stm32f10x_hd.s 文件中可以找到。 函数体里面通过函数: if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) 判断是否接受中断,如果是串口接受中断,则读取串口接受到的数据: Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据 读到数据后接下来就对数据进行分析。 这里我们设计了一个小小的接收协议:通过这个函数,配合一个数组 USART_RX_BUF[],一个接收状态寄存器 USART_RX_STA(此寄存器其实就是一个全局 变量,由作者自行添加。由于它起到类似寄存器的功能,这里暂且称之为寄存器)实现对 串口数据的接收管理。USART_RX_BUF 的大小由 USART_REC_LEN 定义,也就是一次接 收的数据最大不能超过 USART_REC_LEN 个字节。USART_RX_STA 是一个接收状态寄存 器其各的定义如表 5.3.1.1 所示: USART_RX_STA bit15 bit14 bit13~0 接收完成 标志 接收到 0X0D 标志 接收到的有效数据个数 设计思路如下: 表 5.3.1.1 接收状态寄存器位定义表 当接收到从电脑发过来的数据,把接收到的数据保存在 USART_RX_BUF 中,同时在 接收状态寄存器(USART_RX_STA)中计数接收到的有效数据个数,当收到回车(回车的 表示由 2 个字节组成:0X0D 和 0X0A)的第一个字节 0X0D 时,计数器将不再增加,等待 0X0A 的到来,而如果 0X0A 没有来到,则认为这次接收失败,重新开始下一次接收。如 116 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 果顺利接收到 0X0A,则标记 USART_RX_STA 的第 15 位,这样完成一次接收,并等待该 位被其他程序清除,从而开始下一次的接收,而如果迟迟没有收到 0X0D,那么在接收数 据超过 USART_REC_LEN 的时候,则会丢弃前面的数据,重新接收。中断相应函数代码 如下: void USART1_IRQHandler(void) //串口 1 中断服务程序 { u8 Res; #ifdef OS_TICKS_PER_SEC //如果时钟节拍数定义了,说明要使用 ucosII 了. OSIntEnter(); #endif if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是 0x0d 0x0a 结尾) { Res =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据 if((USART_RX_STA&0x8000)==0)//接收未完成 { if(USART_RX_STA&0x4000)//接收到了 0x0d { if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始 else USART_RX_STA|=0x8000; //接收完成了 } else //还没收到 0X0D { if(Res==0x0d)USART_RX_STA|=0x4000; else { USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ; USART_RX_STA++; if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0; //接收数据错误,重新开始接收 } } } } #ifdef OS_TICKS_PER_SEC //如果时钟节拍数定义了,说明要使用 ucosII 了. OSIntExit(); #endif } EN_USART1_RX 和 USART_REC_LEN 都是在 usart.h 文件里面定义的,当需要使用串 口接收的时候,我们只要在 usart.h 里面设置 EN_USART1_RX 为 1 就可以了。不使用的时 候,设置,EN_USART1_RX 为 0 即可,这样可以省出部分 sram 和 flash,我们默认是设置 EN_USART1_RX 为 1,也就是开启串口接收的。 117 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 OS_CRITICAL_METHOD,则是用来判断是否使用 ucos,如果使用了 ucos,则调用 OSIntEnter 和 OSIntExit 函数,如果没有使用 ucos,则不调用这两个函数(这两个函数用于 实现中断嵌套处理,这里我们先不理会)。 118 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三篇 实战篇 经过前两篇的学习,我们对 STM32 开发的软件和硬件平台都有了个比较深入的了解了, 接下来我们将通过实例,由浅入深,带大家一步步的学习 STM32。 STM32 的内部资源非常丰富,对于初学者来说,一般不知道从何开始。本篇将从 STM32 最简单的外设说起,然后一步步深入。每一个实例都配有详细的代码及解释,手把手教你如何 入手 STM32 的各种外设,通过本篇的学习,希望大家能学会 STM32 绝大部分外设的使用。 本篇总共分为 38 章,每一章即一个实例,下面就让我们开始精彩的 STM32 之旅。 119 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第六章 跑马灯实验 STM32 最简单的外设莫过于 IO 口的高低电平控制了,本章将通过一个经典的跑马灯程序, 带大家开启 STM32 之旅,通过本章的学习,你将了解到 STM32 的 IO 口作为输出使用的方法。 在本章中,我们将通过代码控制 ALIENTEK MiniSTM32 开发板上的两个 LED:DS0 和 DS1 交 替闪烁,实现类似跑马灯的效果。 本章分为如下四个小节: 6.1, STM32 IO 口简介 6.2, 硬件设计 6.3, 软件设计 6.4, 仿真与下载 120 6.1 STM32 IO 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 本章将要实现的是控制 ALIENTEK miniSTM32 开发板上的两个 LED 实现一个类似跑马灯 的效果,该实验的关键在于如何控制 STM32 的 IO 口输出。了解了 STM32 的 IO 口如何输出的, 就可以实现跑马灯了。通过这一章的学习,你将初步掌握 STM32 基本 IO 口的使用,而这是迈 向 STM32 的第一步。 这一章节因为是第一个实验章节,所以我们在这一章将讲解一些知识为后面的实验做铺垫。 为了小节标号与后面实验章节一样,这里我们不另起一节来讲。 在讲解 STM32 的 GPIO 之前,首先打开我们光盘的第一个固件库版本实验工程跑马灯实验 工程(光盘目录为“: 4,程序源码\标准例程-V3.5 库函数版本\实验 1 跑马灯/USER/ LED.uvproj”), 可以看到我们的实验工程目录: 图 6.1.1 跑马灯实验目录结构 接下来我们逐一讲解一下我们的工程目录下面的组以及重要文件。 ① 组 FWLib 下面存放的是 ST 官方提供的固件库函数,里面的函数我们可以根据需要添加 和删除,但是一定要注意在头文件 stm32f10x_conf.h 文件中注释掉删除的源文件对应的 121 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 头文件,这里面的文件内容用户不需要修改。 ② 组 CORE 下面存放的是固件库必须的核心文件和启动文件。这里面的文件用户不需要修 改。 ③ 组 SYSTEM 是 ALIENTEK 提供的共用代码,这些代码的作用和讲解在第五章都有讲解, 大家可以翻过去看下。 ④ 组 HARDWARE 下面存放的是每个实验的外设驱动代码,他的实现是通过调用 FWLib 下面的固件库文件实现的,比如 led.c 里面调用 stm32f10x_gpio.c 里面的函数对 led 进行 初始化,这里面的函数是讲解的重点。后面的实验中可以看到会引入多个源文件。 ⑤ 组 USER 下面存放的主要是用户代码。但是 system_stm32f10x.c 文件用户不需要修改, 同时 stm32f10x_it.c 里面存放的是中断服务函数,这两个文件的作用在 3.1 节有讲解,大 家可以翻过去看看。Main.c 函数主要存放的是主函数了,这个大家应该很清楚。 针对第①步中怎么随意添加和删除固件库文件,这里我们稍微讲解一下。 首先从上面的图中可以看到,stm32f10x_gpio.c 源文件下面 include 了好几个头文件,其中有 一个 stm32f10x_conf.h,这个文件会被每个固件库源文件引用。我们可以打开看看里面的内容: 图 6.1.2 stm32f10x_conf 文件内容 从图中可以看出,在头文件 stm32f10x_conf.h 文件中,我们包含了四个.h 头文件,那是因 122 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 为我们的 FWLib 组下面引入了相应的 4 个.c 源文件。同时大家记住,后面三个源文件 stm32f10x_rcc.c,stm32f10x_usart.c 以及 misc.c 在每个实验基本都需要添加。在这个实验中,因 为 LED 是关系到 STM32 的 GPIO,所以我们增加了 stm32f10x_gpio.c 和头文件 stm32f10x_gpio.h 的引入。添加和删除固件库源文件的步骤是: 1. 在 stm32f10x_conf.h 文件 引 入 需 要的 .h 头文 件。 这 些 头 文件 在 每个 实验 的 目 录 \STM32F10x_FWLib\inc 下面都有存放。 2. 在 FWLib 下面加入步骤一中引入的.h 头文件对应的源文件。记住最好一一对应,否则 就有可能会报错。这些源文件在每个实验的\STM32F10x_FWLib\src 目录下面都有存放。 添加方法请参考 3.3 节的内容。 最后我们讲解一下这些组之间的层次结构: 组USER main.c文件 用户代码 调用HARDWARE组下 面的设备驱动代码以 及直接操作FWLib下 面的固件库函数。 组HARDWARE xxx.c/xxx.h文件 设备初始化代码 直接操作FWLib下面 的固件库函数实 现。ALIENTEK提供。 组FWLib stm32f10x_ppp.c stm32f10x_ppp.h 固件库驱动代码 直接操作寄存器实 现。 寄存器 图 6.1.3 代码层次结构图 从层次图中可以看出,我们的用户代码和 HARDWARE 下面的外设驱动代码再不需要直接 操作寄存器,而是直接或间接操作官方提供的固件库函数。但是后面我们的为了让大家更全面 方便的了解外设,我们会增加重要的外设寄存器的讲解,这样对底层知识更加了解,方便我们 深入学习固件库。 准备内容我们就讲解到这里,接下来我们就要进入我们跑马灯实验的讲解部分了。这里需 要说明一下,我们在讲解固件库之前会首先对重要寄存器进行一个讲解,这样是为了大家对寄 存器有个初步的了解。大家学习固件库,并不需要记住每个寄存器的作用,而只是通过了解寄 存器来对外设一些功能有个大致的了解,这样对以后的学习也很有帮助。 首先要提一下,在固件库中,GPIO 端口操作对应的库函数函数以及相关定义在文件 stm32f10x_gpio.h 和 stm32f10x_gpio.c 中。 STM32 的 IO 口相比 51 而言要复杂得多,所以使用起来也困难很多。首先 STM32 的 IO 口 可以由软件配置成如下 8 种模式: 1、输入浮空 2、输入上拉 3、输入下拉 4、模拟输入 5、开漏输出 6、推挽输出 7、推挽式复用功能 8、开漏复用功能 每个 IO 口可以自由编程,但 IO 口寄存器必须要按 32 位字被访问。STM32 的很多 IO 口都 是 5V 兼容的,这些 IO 口在与 5V 电平的外设连接的时候很有优势,具体哪些 IO 口是 5V 兼容 的,可以从该芯片的数据手册管脚描述章节查到(I/O Level 标 FT 的就是 5V 电平兼容的)。 123 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 STM32 的每个 IO 端口都有 7 个寄存器来控制。他们分别是:配置模式的 2 个 32 位的端口 配置寄存器 CRL 和 CRH;2 个 32 位的数据寄存器 IDR 和 ODR;1 个 32 位的置位/复位寄存器 BSRR;一个 16 位的复位寄存器 BRR;1 个 32 位的锁存寄存器 LCKR。大家如果想要了解每个 寄存器的详细使用方法,可以参考《STM32 中文参考手册 V10》P105~P129。 CRL 和 CRH 控制着每个 IO 口的模式及输出速率。 STM32 的 IO 口位配置表如表 6.1.4 所示: 表 6.1.4 STM32 的 IO 口位配置表 STM32 输出模式配置如表 6.1.5 所示: 表 6.1.5 STM32 输出模式配置表 接下来我们看看端口低配置寄存器 CRL 的描述,如图 6.1.6 所示: 124 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.1.6 端口低配置寄存器 CRL 各位描述 该寄存器的复位值为 0X4444 4444,从图 6.1.4 可以看到,复位值其实就是配置端口为浮空 输入模式。从上图还可以得出:STM32 的 CRL 控制着每组 IO 端口(A~G)的低 8 位的模式。 每个 IO 端口的位占用 CRL 的 4 个位,高两位为 CNF,低两位为 MODE。这里我们可以记住几 个常用的配置,比如 0X0 表示模拟输入模式(ADC 用)、0X3 表示推挽输出模式(做输出口用, 50M 速率)、0X8 表示上/下拉输入模式(做输入口用)、0XB 表示复用输出(使用 IO 口的第二 功能,50M 速率)。 CRH 的作用和 CRL 完全一样,只是 CRL 控制的是低 8 位输出口,而 CRH 控制的是高 8 位输出口。这里我们对 CRH 就不做详细介绍了。下面我们讲解一下怎样通过固件库设置 GPIO 的相关参数和输出。 GPIO 相关的函数和定义分布在固件库文件 stm32f10x_gpio.c 和头文件 stm32f10x_gpio.h 文 件中。 在固件库开发中,操作寄存器 CRH 和 CRL 来配置 IO 口的模式和速度是通过 GPIO 初始化 函数完成: void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct); 这个函数有两个参数,第一个参数是用来指定 GPIO,取值范围为 GPIOA~GPIOG。 第二个参数为初始化参数结构体指针,结构体类型为 GPIO_InitTypeDef。下面我们看看这个结 构体的定义。首先我们打开我们光盘的跑马灯实验,然后找到 FWLib 组下面的 stm32f10x_gpio.c 文件,定位到 GPIO_Init 函数体处,双击入口参数类型 GPIO_InitTypeDef 后右键选择“Go to definition of …”可以查看结构体的定义: typedef struct 125 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { uint16_t GPIO_Pin; GPIOSpeed_TypeDef GPIO_Speed; GPIOMode_TypeDef GPIO_Mode; }GPIO_InitTypeDef; 下面我们通过一个 GPIO 初始化实例来讲解这个结构体的成员变量的含义。 通过初始化结构体初始化 GPIO 的常用格式是: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //LED0-->PA.8 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//速度 50MHz GPIO_Init(GPIOA, &GPIO_InitStructure);//根据设定参数配置 GPIO 上面代码的意思是设置 GPIOB 的第 5 个端口为推挽输出模式,同时速度为 50M。从上面初始 化代码可以看出,结构体 GPIO_InitStructure 的第一个成员变量 GPIO_Pin 用来设置是要初始化 哪个或者哪些 IO 口;第二个成员变量 GPIO_Mode 是用来设置对应 IO 端口的输出输入模式, 这些模式是上面我们讲解的 8 个模式,在 MDK 中是通过一个枚举类型定义的: typedef enum { GPIO_Mode_AIN = 0x0, //模拟输入 GPIO_Mode_IN_FLOATING = 0x04, //浮空输入 GPIO_Mode_IPD = 0x28, //下拉输入 GPIO_Mode_IPU = 0x48, //上拉输入 GPIO_Mode_Out_OD = 0x14, //开漏输出 GPIO_Mode_Out_PP = 0x10, //通用推挽输出 GPIO_Mode_AF_OD = 0x1C, //复用开漏输出 GPIO_Mode_AF_PP = 0x18 //复用推挽 }GPIOMode_TypeDef; 第三个参数是 IO 口速度设置,有三个可选值,在 MDK 中同样是通过枚举类型定义: typedef enum { GPIO_Speed_10MHz = 1, GPIO_Speed_2MHz, GPIO_Speed_50MHz }GPIOSpeed_TypeDef; 这些入口参数的取值范围怎么定位,怎么快速定位到这些入口参数取值范围的枚举类型, 在我们上面章节 4.7 的“快速组织代码”章节有讲解,不明白的朋友可以翻回去看一下,这里 我们就不重复讲解,在后面的实验中,我们也不再去重复讲解怎么定位每个参数的取值范围的 方法。 IDR 是一个端口输入数据寄存器,只用了低 16 位。该寄存器为只读寄存器,并且只能以 16 位的形式读出。该寄存器各位的描述如图 6.1.7 所示: 126 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.1.7 端口输入数据寄存器 IDR 各位描述 要想知道某个 IO 口的电平状态,你只要读这个寄存器,再看某个位的状态就可以了。使 用起来是比较简单的。 在固件库中操作 IDR 寄存器读取 IO 端口数据是通过 GPIO_ReadInputDataBit 函数实现的: uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 比如我要读 GPIOA.5 的电平状态,那么方法是: GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5); 返回值是 1(Bit_SET)或者 0(Bit_RESET); ODR 是一个端口输出数据寄存器,也只用了低 16 位。该寄存器为可读写,从该寄存器读 出来的数据可以用于判断当前 IO 口的输出状态。而向该寄存器写数据,则可以控制某个 IO 口 的输出电平。该寄存器的各位描述如图 6.1.8 所示: 图 6.1.8 端口输出数据寄存器 ODR 各位描述 在固件库中设置 ODR 寄存器的值来控制 IO 口的输出状态是通过函数 GPIO_Write 来实现 的: void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); 该函数一般用来往一次性一个 GPIO 的多个端口设值。 BSRR 寄存器是端口位设置/清除寄存器。该寄存器和 ODR 寄存器具有类似的作用,都可 以用来设置 GPIO 端口的输出位是 1 还是 0。下面我们看看该寄存器的描述如下图: 127 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.1.9 端口位设置/清除寄存器 BSRR 各位描述 该寄存器通过举例子可以很清楚了解它的使用方法。例如你要设置 GPIOA 的第 1 个端口 值为 1,那么你只需要往寄存器 BSRR 的低 16 位对应位写 1 即可: GPIOA->BSRR=1<<1; 如果你要设置 GPIOA 的第 1 个端口值为 0,你只需要往寄存器高 16 位对应为写 1 即可: GPIOA->BSRR=1<<(16+1) 该寄存器往相应位写 0 是无影响的,所以我们要设置某些位,我们不用管其他位的值。 BRR 寄存器是端口位清除寄存器。该寄存器的作用跟 BSRR 的高 16 位雷同,这里就不做 详细讲解。在 STM32 固件库中,通过 BSRR 和 BRR 寄存器设置 GPIO 端口输出是通过函数 GPIO_SetBits()和函数 GPIO_ResetBits()来完成的。 void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin); void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) 在多数情况下,我们都是采用这两个函数来设置 GPIO 端口的输入和输出状态。比如我们要设 置 GPIOB.5 输出 1,那么方法为: GPIO_SetBits(GPIOB, GPIO_Pin_5); 反之如果要设置 GPIOB.5 输出位 0,方法为: GPIO_ResetBits (GPIOB, GPIO_Pin_5); GPIO 相关的函数我们先讲解到这里。虽然 IO 操作步骤很简单,这里我们还是做个概括性 的总结,操作步骤为: 1) 使能 IO 口时钟。调用函数为 RCC_APB2PeriphClockCmd()。 2) 初始化 IO 参数。调用函数 GPIO_Init(); 3) 操作 IO。操作 IO 的方法就是上面我们讲解的方法。 上面我们讲解了 STM32 IO 口的基本知识以及固件库操作 GPIO 的一些函数方法,下面我 们来讲解我们的跑马灯实验的硬件和软件设计。 6.2 硬件设计 本章用到的硬件只有 LED(DS0 和 DS1)。其电路在 ALIENTEK MiniSTM32 开发板上默认 是已经连接好了的。DS0 接 PA8,DS1 接 PD2。所以在硬件上不需要动任何东西。其连接原理 128 图如图 6.2.1 下: STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.2.1 LED 与 STM32 连接原理图 6.3 软件设计 这是我们的第一个实验,所以我教大家怎么从我们前面讲解的 Template 工程一步一步加入 我们的固件库以及我们的 led 相关的驱动函数到我们工程,使之跟我们光盘的跑马灯实验工程 一模一样。首先大家打开我们 3.3.3 小节新建的 V3.5 版本的工程模板。如果您还没有新建,也 可以直接打开我们光盘已经新建好了的工程模板,路径为:“\4,程序源码\标准例程-V3.5 库函 数版本\实验 0 Template 工程模板”。注意,是直接点击工程下面的 USER 目录下面的 Template.uvproj。 大家可以看到,我们模板里面的 FWLIB 下面,我们引入了所有的固件库源文件和对应的 头文件: 129 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.3.1 实际上,这些大家可以根据工程需要添加,比如我们跑马灯实验,我们并没有用到 ADC, 自然我们可以去掉 stm32f10x_adc.c,这样可以减少工程编译时间。 跑马灯实验我们主要用到的固件库文件是: stm32f10x_gpio.c /stm32f10x_gpio.h stm32f10x_rcc.c/stm32f10x_rcc.h misc.c/ misc.h stm32f10x_usart /stm32f10x_usart.h 其中 stm32f10x_rcc.h 头文件在每个实验中都要引入,因为系统时钟配置函数以及相关的外设时 钟使能函数都在这个其源文件 stm32f10x_rcc.c 中。stm32f10x_usart.h 和 misc.h 头文件在我们 SYSTEM 文件夹中都需要使用到,所以每个实验都会引用。 在 stm32f10x_conf.h 文件里面,我们注释掉其他不用的头文件,只引入以下头文件: #include "stm32f10x_gpio.h" #include "stm32f10x_rcc.h" #include "stm32f10x_usart.h" #include "misc.h" 所以,首先,我们打开 stm32f10x_conf.h 文件,注释掉不需要用的,只引入这四个头文件: 图 6.3.2 接下来,我们去掉多余的其他的源文件,方法如下图,右击 Template,选择“Manage project Items”,进入这个选项卡: 130 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.3.3 我们选中“FWLIB”分组,然后选中不需要的源文件点击删除按钮删掉,留下下图中我们 使用到的四个源文件,然后点击 OK: 图 6.3.4 这样我们的工程 FWLIB 下面只剩下四个源文件: 131 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.3.5 然后我们进入我们工程的目录,在工程根目录文件夹下面新建一个 HARDWARE 的文件 夹,用来存储以后与硬件相关的代码。然后在 HARDWARE 文件夹下新建一个 LED 文件夹, 用来存放与 LED 相关的代码。如图 6.3.6 所示: 图 6.3.6 新建 HARDWARE 文件夹 接下来,我们回到我们的工程(如果是使用的上面新建的工程模板,那么就是 Template.uvproj,大家可以将其重命名为 LED.uvproj),按 按钮新建一个文件,然后保存在 HARDWARE->LED 文件夹下面,保存为 led.c。在该文件中输入如下代码(代码大家可以直接打 132 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 开我们光盘的跑马灯实验,从相应的文件中间复制过来): #include "led.h" //LED IO 初始化 void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOD, ENABLE); //使能 PA,PD 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //LED0-->PA.8 端口配置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO 口速度为 50MHz GPIO_Init(GPIOA, &GPIO_InitStructure); //根据设定参数初始化 GPIOA.8 GPIO_SetBits(GPIOA,GPIO_Pin_8); //PA.8 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; GPIO_Init(GPIOD, &GPIO_InitStructure); GPIO_SetBits(GPIOD,GPIO_Pin_2); } //LED1-->PD.2 端口配置, 推挽输出 //推挽输出 ,IO 口速度为 50MHz //PD.2 输出高 图 6.3.7 该代码里面就包含了一个函数 void LED_Init(void),该函数的功能就是用来实现配置 PA8 和 PD2 为推挽输出。这里需要注意的是:在配置 STM32 外设的时候,任何时候都要先使能该 133 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 外设的时钟!GPIO 是挂载在 APB2 总线上的外设,在固件库中对挂载在 APB2 总线上的外设时 钟使能是通过函数 RCC_APB2PeriphClockCmd()来实现的。对于这个入口参数设置,在我们前 面的“快速组织代码”章节已经讲解很清楚了。看看我们的代码: RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA| RCC_APB2Periph_GPIOD, ENABLE); //使能 GPIOA,GPIOD 端口时钟 这行代码的作用是使能 APB2 总线上的 GPIOA 和 GPIOD 的时钟。 在配置完时钟之后,LED_Init 配置了 GPIOA.8 和 GPIOD.2 的模式为推挽输出,并且默认 输出 1。这样就完成了对这两个 IO 口的初始化。函数代码是: GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_8); //LED0-->PA.8 端口配置 //推挽输出 //IO 口速度为 50MHz //根据设定参数初始化 GPIOA.8 //PA.8 输出高 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2; //LED1-->PD.2 端口配置, 推挽输出 GPIO_Init(GPIOD, &GPIO_InitStructure); //推挽输出 ,IO 口速度为 50MHz GPIO_SetBits(GPIOD,GPIO_Pin_2); //PD.2 输出高 这里需要说明的是,因为 GPIOA 和 GPIOD 的 IO 口的初始化参数都是设置在结构体变量 GPIO_InitStructure 中,因为两个 IO 口的模式和速度都一样,所以我们只用初始化一次,在 GPIOD.2 的初始化的时候就不需要再重复初始化速度和模式了。最后一行代码: GPIO_SetBits(GPIOD,GPIO_Pin_2); 的作用是在初始化中将 GPIOD.2 输出设置为高。 保存 led.c 代码,然后我们按同样的方法,新建一个 led.h 文件,也保存在 LED 文件夹下面。 在 led.h 中输入如下代码: #ifndef __LED_H #define __LED_H #include "sys.h" //LED 端口定义 #define LED0 PAout(8) // PA8 #define LED1 PDout(2) // PD2 void LED_Init(void);//初始化 #endif 这段代码里面最关键就是 2 个宏定义: #define LED0 PAout(8) // PA8 #define LED1 PDout(2) // PD2 这里使用的是位带操作来实现操作某个 IO 口的 1 个位的,关于位带操作前面第五章 5.2.1 已经有介绍,这里不再多说。需要说明的是,这里同样可以使用固件库操作来实现 IO 口操作。 如下: GPIO_SetBits(GPIOA, GPIO_Pin_8); //设置 GPIOA.8 输出 1,等同 LED0=1; GPIO_ResetBits (GPIOA, GPIO_Pin_8); //设置 GPIOA.8 输出 0,等同 LED0=0; 有兴趣的朋友不妨修改我们的位带操作为库函数直接操作,这样也有利于学习。 将 led.h 也保存一下。接着,我们在 Manage Components 管理里面新建一个 HARDWARE 134 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 的组,并把 led.c 加入到这个组里面,如图 6.3.8 所示: 图 6.3.8 给工程新增 HARDWARE 组 单击 OK,回到工程,然后你会发现在 Project Workspace 里面多了一个 HARDWARE 的组, 在改组下面有一个 led.c 的文件。如图 6.3.9 所示: 图 6.39 新增 HARDWARE 组 然后用之前介绍的方法(在 3.3.3 节介绍的)将 led.h 头文件的路径加入到工程里面。 135 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.3.10 回到主界面,在 main 函数里面编写如下代码: #include "led.h" #include "delay.h" #include "sys.h" //ALIENTEK miniSTM32 开发板实验 1 //跑马灯实验 int main(void) { delay_init(); LED_Init(); //延时函数初始化 //初始化与 LED 连接的硬件接口 while(1) { LED0=0; LED1=1; delay_ms(300); //延时 300ms LED0=1; LED1=0; delay_ms(300); //延时 300ms } } 代码包含了#include "led.h"这句,使得 LED0、LED1、LED_Init 等能在 main()函数里被调用。 136 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 这里我们需要重申的是,在固件库 V3.5 中,系统在启动的时候会调用 system_stm32f10x.c 中的 函数 SystemInit()对系统时钟进行初始化,在时钟初始化完毕之后会调用 main()函数。 所以我 们不需要再在 main()函数中调用 SystemInit()函数。当然如果有需要重新设置时钟系统,可以写 自己的时钟设置代码,SystemInit()只是将时钟系统初始化为默认状态。 main()函数非常简单,先调用 delay_init()初始化延时,接着就是调用 LED_Init()来初始化 GPIOA.8 和 GPIOD.2 为输出。最后在死循环里面实现 LED0 和 LED1 交替闪烁,间隔为 300ms。 上面是通过位带操作实现的 IO 操作,我们也可以修改 main()函数,直接通过库函数来操作 IO 达到同样的效果,大家不妨试试。 int main(void) { delay_init(); LED_Init(); while(1) { GPIO_ResetBits(GPIOA,GPIO_Pin_8); GPIO_SetBits(GPIOD,GPIO_Pin_2); delay_ms(300); //延时 300ms GPIO_SetBits(GPIOA,GPIO_Pin_8); GPIO_ResetBits(GPIOD,GPIO_Pin_2); delay_ms(300); //延时 300ms //延时函数初始化 //初始化与 LED 连接的硬件接口 // PA8 输出低,LED0=0; // PD2 输出高,LED1=1; //PA8 输出高,LED0=1; // PD2 输出低,LED1=0; } } 将主函数替换为上面代码,然后重新执行,可以看到,结果跟用位带操作一样的效果。大 家可以对比一下。这个代码在我们光盘的实验代码“实验 1 跑马灯-库函数操作”中。 然后按 ,编译工程,得到结果如图 6.3.11 所示: 图 6.3.11 编译结果 可以看到没有错误,也没有警告。从编译信息可以看出,我们的代码占用 FLASH 大小为: 4884 字节(4548+336),所用的 SRAM 大小为:1888 个字节(1836+52)。 这里我们解释一下,编译结果里面的几个数据的意义: Code:表示程序所占用 FLASH 的大小(FLASH)。 RO-data:即 Read Only-data,表示程序定义的常量(FLASH)。 RW-data:即 Read Write-data,表示已被初始化的变量(SRAM) ZI-data:即 Zero Init-data,表示未被初始化的变量(SRAM) 有了这个就可以知道您当前使用的 flash 和 sram 大小了,所以,一定要注意的是程序的大 小不是.hex 文件的大小,而是编译后的 Code 和 RO-data 之和。另外,这里看到 SRAM 用了 1888 137 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 字节,可能有点恐怖,其实在 startup_stm32f10x_hd.s 里面,我们定义了堆栈(Heap+Stack)大小 为 0X600,也就是 1536 字节,usart.c 里面定义了 200 字节大小的接收缓冲,这样就去了 1736 字节了,本例程实际没用到多少 SRAM。 接下来,我们就先进行软件仿真,验证一下是否有错误的地方,然后下载到 miniSTM32 开发板看看实际运行的结果。 6.4 仿真与下载 此代码,我们先进行软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到 ALIENTEKminiSTM32 板子上面看运行是否正确。 首先,我们进行软件仿真(请先确保 Options for Target Debug 选项卡里面已经设置为 Use Simulator)。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup,新建两个信号 PORTA.8 和 PORTD.2,如图 6.4.1 所示: 图 6.4.1 逻辑分析设置 Display Type 选择 bit,然后单击 Close 关闭该对话框,可以看到逻辑分析窗口出来了两个 信号,如图 6.4.2 所示: 138 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.4.2 设置后的逻辑分析窗口 接着,点击 ,开始运行。运行一段时间之后,按 可以看到如图 6.4.3 所示的波形: 按钮,暂停仿真回到逻辑分析窗口, 图 6.4.3 仿真波形 这里注意 Gird 要调节到 0.25s 左右比较合适,可以通过 Zoom 里面的 In 按钮来放大波形, 通过 Out 按钮来缩小波形,或者按 All 显示全部波形。从上图中可以看到 PORTA.8 和 PORTD.2 交替输出,周期可以通过中间那根红线来测量。至此,我们的软件仿真已经顺利通过。 在软件仿真没有问题了之后,我们就可以把代码下载到开发板上,看看运行结果是否与我 139 们仿真的一致。运行结果如图 6.4.4 所示: STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 6.4.4 执行结果 至此,我们的第一章的学习就结束了,本章作为 STM32 的入门第一个例子,详细介绍了 STM32 的 IO 口操作,同时巩固了前面的学习,并进一步介绍了 MDK 的软件仿真功能。希望 大家好好理解一下。 140 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第七章 按键输入实验 上一章,我们介绍了 STM32 的 IO 口作为输出的使用,这一章,我们将向大家介绍如何使 用 STM32 的 IO 口作为输入用。在本章中,我们将利用板载的 3 个按键,来控制板载的两个 LED 的亮灭。通过本章的学习,你将了解到 STM32 的 IO 口作为输入口的使用方法。本章分为如下 几个小节: 7.1 STM32 IO 口简介 7.2 硬件设计 7.3 软件设计 7.4 仿真与下载 141 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 7.1 STM32 IO 口简介 STM32 的 IO 口在上一章已经有了比较详细的介绍,这里我们不再多说。STM32 的 IO 口 做输入使用的时候,是通过调用函数 GPIO_ReadInputDataBit()来读取 IO 口的状态的。了解了 这点,就可以开始我们的代码编写了。 这一节,我们将通过 MiniSTM32 开发板上载有的 3 个按钮(KEY0/KEY1/WK_UP),来控 制板上的 2 个 LED,其中 KEY0 控制 DS0,按一次亮,再按一次,就灭。KEY1 控制 DS1,效 果同 KEY0。WK_UP 按键则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。 。 7.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0、DS1 2) 3 个按键:KEY0、KEY1 和 KEY_UP。 DS0、DS1 和 STM32 的连接在上一章已经介绍了,在 MiniSTM32 开发板上的按键 KEY0 连接在 PC5 上、KEY1 连接在 PA15 上、WK_UP 连接在 PA0 上。如图 7.2.1 所示: 图 7.2.1 按键与 STM32 连接原理图 这里需要注意的是:KEY0 和 KEY1 是低电平有效的,而 WK_UP 是高电平有效的,除了 KEY1 有上拉电阻(与 JTDI 共用),其他两个都没有上下拉电阻,所以,需要在 STM32 内部设 置上下拉。 7.3 软件设计 从这章开始,我们的软件设计主要是通过直接打开我们光盘的实验工程,而不再讲解怎么 加入文件和头文件目录。 打开我们的按键实验工程可以看到,我们引入了 key.c 文件以及头文件 key.h。下面我们首 先打开 key.c 文件,代码如下: #include "key.h" #include "delay.h" //按键初始化函数 //PA15 和 PC5 设置成输入 void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; 142 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC, ENABLE);//使能 PORTA,PORTC 时钟 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE); //关闭 jtag,使能 SWD,可以用 SWD 模式调试 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15;//PA15 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化 GPIOA15 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;//PC5 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //设置成上拉输入 GPIO_Init(GPIOC, &GPIO_InitStructure);//初始化 GPIOC5 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//PA0 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 设置成输入,默认下拉 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化 GPIOA.0 } //按键处理函数 //返回按键值 //mode:0,不支持连续按;1,支持连续按; //返回值: //0,没有任何按键按下 //KEY0_PRES,KEY0 按下 //KEY1_PRES,KEY1 按下 //WKUP_PRES,WK_UP 按下 //注意此函数有响应优先级,KEY0>KEY1>WK_UP!! u8 KEY_Scan(u8 mode) { static u8 key_up=1;//按键按松开标志 if(mode)key_up=1; //支持连按 if(key_up&&(KEY0==0||KEY1==0||WK_UP==1)) { delay_ms(10);//去抖动 key_up=0; if(KEY0==0)return KEY0_PRES; else if(KEY1==0)return KEY1_PRES; else if(WK_UP==1)return WKUP_PRES; }else if(KEY0==1&&KEY1==1&&WK_UP==0)key_up=1; return 0;// 无按键按下 } 这段代码包含 2 个函数,void KEY_Init(void)和 u8 KEY_Scan(u8 mode),KEY_Init 是用来 初 始 化 按 键 输 入 的 IO 口 的 。 实 现 PA0 、PA15 和 PC5 的 输 入 设 置, 注 意 这 调 用 了 : 143 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这个函数,用于禁止 JTAG, 开启 SWD,因为 PA15 占用了 JTAG 的一个 IO,所以要禁止 JTAG,从而让 PA15 用作普通 IO 输入。 KEY_Scan 函数,则是用来扫描这 3 个 IO 口是否有按键按下。KEY_Scan 函数,支持两种 扫描方式,通过 mode 参数来设置。 当 mode 为 0 的时候,KEY_Scan 函数将不支持连续按,扫描某个按键,该按键按下之后必 须要松开,才能第二次触发,否则不会再响应这个按键,这样的好处就是可以防止按一次多次 触发,而坏处就是在需要长按的时候就不合适了。 当 mode 为 1 的时候,KEY_Scan 函数将支持连续按,如果某个按键一直按下,则会一直返 回这个按键的键值,这样可以方便的实现长按检测。 有了 mode 这个参数,大家就可以根据自己的需要,选择不同的方式。这里要提醒大家, 因为该函数里面有 static 变量,所以该函数不是一个可重入函数,在有 OS 的情况下,这个大家 要留意下。同时还有一点要注意的就是,该函数的按键扫描是有优先级的,最优先的是 KEY0, 第二优先的是 KEY1,最后是 WK_UP 按键。该函数有返回值,如果有按键按下,则返回非 0 值,如果没有或者按键不正确,则返回 0。 接下来我们看看头文件 key.h 里面的代码。 #ifndef __KEY_H #define __KEY_H #include "sys.h" #define KEY0 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)//读取按键 0 #define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取按键 1 #define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键 2 #define KEY0_PRES 1 #define KEY1_PRES 2 #define WKUP_PRES 3 //KEY0 //KEY1 //WK_UP void KEY_Init(void);//IO 初始化 u8 KEY_Scan(u8 mode); //按键扫描函数 #endif 这段代码里面最关键就是 3 个宏定义: #define KEY0 GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)//读取按键 0 #define KEY1 GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_15)//读取按键 1 #define WK_UP GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)//读取按键 2 前面跑马灯实验用的是位带操作实现设定某个 IO 口的位。这里我们采取的是库函数的读 取 IO 口的值。当然,上面的功能也同样可以通过位带操作来简单的实现: //#define KEY0 PCin(5) //#define KEY1 PAin(15) //#define WK_UP PAin(0) 用库函数实现的好处是在各个 STM32 芯片上面的移植性非常好,不需要修改任何代码。 用位带操作的好处是简洁,至于使用哪种方法,看各位的爱好了。以后我们实例比较多地方用 到位带操作,这里大家要注意一下。 在 key.h 中,我们还定义了 KEY0_PRES / KEY1_PRES / KEYUP_PRES 等 3 个宏定义,分 144 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 别对应开发板的 KEY0、KEY1 和 WK_UP 按键按下时 KEY_Scan 的返回值。通过这些宏定义, 可以方便大家记忆和使用。 最后,我们看看 main.c 里面编写的主函数代码如下: #include "led.h" #include "delay.h" #include "sys.h" #include "key.h" //ALIENTEK Mini STM32 开发板范例代码 2 //按键输入实验 //技术支持:www.openedv.com //广州市星翼电子科技有限公司 int main(void) { u8 t; delay_init(); LED_Init(); KEY_Init(); LED0=0; while(1) //延时函数初始化 //初始化与 LED 连接的硬件接口 //初始化与按键连接的硬件接口 //点亮 LED { t=KEY_Scan(0); switch(t) //得到键值 { case KEY0_PRES: LED0=!LED0; break; case KEY1_PRES: LED1=!LED1; break; case WKUP_PRES: LED0=!LED0; LED1=!LED1; break; default: delay_ms(10); } } } 然后按 ,编译工程,得到结果如图 7.3.3 所示: 145 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 7.3.3 编译结果 可以看到没有错误,也没有警告。接下来,我们还是先进行软件仿真,验证一下是否有错 误的地方,然后才下载到 MiniSTM32 开发板看看实际运行的结果。 7.4 仿真与下载 我们可以先用软件仿真,看看结果对不对,根据软件仿真的结果,然后再下载到 Mini STM32 板子上面看运行是否正确。 首先,我们进行软件仿真。先按 开始仿真,接着按 ,显示逻辑分析窗口,点击 Setup, 新建 5 个信号 PORTA.8、PORTD.2、PORTC.5、PORTA.15 和 PORTA.0,如图 7.4.1 所示: 图 7.4.1 新建仿真信号 然后再点击 PeripheralsGeneral Purpose I/OGPIOA,弹出 GPIOA 的查看窗口,如图 7.4.2 所示: 146 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 7.4.2 查看 GPIOA 寄存器 然后在 t=KEY_Scan();这里设置一个断点,按 直接执行到这里,然后在 General Purpose I/O A 窗口内的 Pins 里面勾选第 15 位,第 0 位不勾选,这是虽然我们已经设置了这几个 IO 口 为上拉输入,但是 MDK 不会考虑 STM32 自带的上拉和下拉,所以我们得自己手动设置一下, 来使得其初始状态和外部硬件的状态一摸一样,同样的方法,我们勾选 General Purpose I/O C 的 Pins 第 5 位。如图 7.4.3 所示: 147 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 7.4.3 执行到断点处 接着我们执行过这句,可以看到 t 的值依旧为 0,也就是没有任何按键按下。接着我们再 按 ,再次执行到 t=KEY_Scan();我们此次把 GPIOAPins 的 15 位取消勾选,再次执行过这 句,得到 t 的值为 2,如图 7.4.4 所示: 148 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 7.4.4 按键扫描结果 然后按相似的方法,勾选 PA0(PA15 先还原),再取消勾选 PC5(PA0 先还原),最后把 PC5 还原,可以看到逻辑分析窗口的波形如图 7.4.5 所示: 图 7.4.5 仿真波形 149 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 从图 7.4.5 可以看出,当 PA15(KEY1)按下的时候 PD2 翻转,PA0(WK_UP)按下的时候 PD2 和 PA8 都翻转,PC5(KEY0)按下的时候 PA8 翻转,是我们想要得到的结果。因此,可以确 定软件仿真基本没有问题了。接下来可以把代码下载到战 STM32 开发板上看看运行结果是否 正确。 在下载完之后,我们可以按 KEY0、KEY1 和 WK_UP 来看看 DS0 和 DS1 的变化,是否和 我们仿真的结果一致(结果肯定是一致的)。 特别注意:因为 PA0 用作按键输入,而且是高电平有效的,所以一点不要把 PA0 和 1820 的跳线帽短接在一起了,否则会按键“失灵”。 至此,我们的本章的学习就结束了。本章,作为 STM32 的入门第二个例子,介绍了 STM32 的 IO 作为输入的使用方法,同时巩固了前面的学习。希望大家在开发板上实际验证一下,从 而加深印象。 150 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第八章 串口实验 前面两章介绍了 STM32 的 IO 口操作。这一章我们将学习 STM32 的串口,教大家如何使 用 STM32 的串口来发送和接收数据。本章将实现如下功能:STM32 通过串口和上位机的对话, STM32 在收到上位机发过来的字符串后,原原本本的返回给上位机。本章分为如下几个小节: 8.1 STM32 串口简介 8.2 硬件设计 8.3 软件设计 8.4 下载验证 151 8.1 STM32 串口简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 串口作为 MCU 的重要外部接口,同时也是软件开发重要的调试手段,其重要性不言而喻。 现在基本上所有的 MCU 都会带有串口,STM32 自然也不例外。 STM32 的串口资源相当丰富的,功能也相当强劲。ALIENTEK MiniSTM32 开发板所使用 的 STM32F103RCT6 最多可提供 5 路串口,有分数波特率发生器、支持同步单线通信和半双工 单线通讯、支持 LIN、支持调制解调器操作、智能卡协议和 IrDA SIR ENDEC 规范、具有 DMA 等。 5.3 节对串口有过简单的介绍,大家看这个实验的时候记得翻过去看看。接下来我们将主要 从库函数操作层面结合寄存器的描述,告诉你如何设置串口,以达到我们最基本的通信功能。 本章,我们将实现利用串口 1 不停的打印信息到电脑上,同时接收从串口发过来的数据,把发 送过来的数据直接送回给电脑。miniSTM32 开发板板载了 1 个 USB 串口和 1 个 RS232 串口, 我们本章介绍的是通过 USB 串口和电脑通信。 在 4.4.1 章节端口复用功能已经讲解过,对于复用功能的 IO,我们首先要使能 GPIO 时钟, 然后使能复用功能时钟,同时要把 GPIO 模式设置为复用功能对应的模式(这个可以查看手册 《STM32 中文参考手册 V10》P110 的表格“8.1.11 外设的 GPIO 配置”)。这些准备工作做完之后, 剩下的当然是串口参数的初始化设置,包括波特率,停止位等等参数。在设置完成只能接下来 就是使能串口,这很容易理解。同时,如果我们开启了串口的中断,当然要初始化 NVIC 设置中 断优先级别,最后编写中断服务函数。 串口设置的一般步骤可以总结为如下几个步骤: 1) 串口时钟使能,GPIO 时钟使能 2) 串口复位 3) GPIO 端口模式设置 4) 串口参数初始化 5) 开启中断并且初始化 NVIC(如果需要开启中断才需要这个步骤) 6) 使能串口 7) 编写中断处理函数 下面,我们就简单介绍下这几个与串口基本配置直接相关的几个固件库函数。这些函数和 定义主要分布在 stm32f10x_usart.h 和 stm32f10x_usart.c 文件中。 1.串口时钟使能。串口是挂载在 APB2 下面的外设,所以使能函数为: RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1); 2.串口复位。当外设出现异常的时候可以通过复位设置,实现该外设的复位,然后重新配置 这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,都会先执行复位该外 设的操作。复位的是在函数 USART_DeInit()中完成: void USART_DeInit(USART_TypeDef* USARTx);//串口复位 比如我们要复位串口 1,方法为: USART_DeInit(USART1); //复位串口 1 3.串口参数初始化。串口初始化是通过 USART_Init()函数实现的, void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct); 这个函数的的第一个入口参数是指定初始化的串口标号,这里选择 USART1。 第二个入口参数是一个 USART_InitTypeDef 类型的结构体指针,这个结构体指针的成员变量用 来设置串口的一些参数。一般的实现格式为: USART_InitStructure.USART_BaudRate = bound; //一般设置为 9600; USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为 8 位数据格式 152 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 USART_InitStructure.USART_StopBits = USART_StopBits_1; //一个停止位 USART_InitStructure.USART_Parity = USART_Parity_No; //无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式 USART_Init(USART1, &USART_InitStructure); //初始化串口 从上面的初始化格式可以看出初始化需要设置的参数为:波特率,字长,停止位,奇偶校验位, 硬件数据流控制,模式(收,发)。我们可以根据需要设置这些参数。 4.数据发送与接收。STM32 的发送与接收是通过数据寄存器 USART_DR 来实现的,这是 一个双寄存器,包含了 TDR 和 RDR。当向该寄存器写数据的时候,串口就会自动发送,当收 到收据的时候,也是存在该寄存器内。 STM32 库函数操作 USART_DR 寄存器发送数据的函数是: void USART_SendData(USART_TypeDef* USARTx, uint16_t Data); 通过该函数向串口寄存器 USART_DR 写入一个数据。 STM32 库函数操作 USART_DR 寄存器读取串口接收到的数据的函数是: uint16_t USART_ReceiveData(USART_TypeDef* USARTx); 通过该函数可以读取串口接受到的数据。 5.串口状态。串口的状态可以通过状态寄存器 USART_SR 读取。USART_SR 的各位描述如 图 8.1.1 所示: 图 8.1.1 USART_SR 寄存器各位描述 这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。 RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并 且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将 该位清零,也可以向该位写 0,直接清除。 TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如 果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_SR,写 USART_DR。2)直接向该位写 0。 状态寄存器的其他位我们这里就不做过多讲解,大家需要可以查看中文参考手册。 在我们固件库函数里面,读取串口状态的函数是: FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG); 这个函数的第二个入口参数非常关键,它是标示我们要查看串口的哪种状态,比如上面讲解的 RXNE(读数据寄存器非空)以及 TC(发送完成)。例如我们要判断读寄存器是否非空(RXNE),操 作库函数的方法是: USART_GetFlagStatus(USART1, USART_FLAG_RXNE); 我们要判断发送是否完成(TC),操作库函数的方法是: USART_GetFlagStatus(USART1, USART_FLAG_TC); 这些标识号在 MDK 里面是通过宏定义定义的: 153 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define USART_IT_PE ((uint16_t)0x0028) #define USART_IT_TXE ((uint16_t)0x0727) #define USART_IT_TC ((uint16_t)0x0626) #define USART_IT_RXNE ((uint16_t)0x0525) #define USART_IT_IDLE ((uint16_t)0x0424) #define USART_IT_LBD ((uint16_t)0x0846) #define USART_IT_CTS ((uint16_t)0x096A) #define USART_IT_ERR ((uint16_t)0x0060) #define USART_IT_ORE ((uint16_t)0x0360) #define USART_IT_NE ((uint16_t)0x0260) #define USART_IT_FE ((uint16_t)0x0160) 6.串口使能。串口使能是通过函数 USART_Cmd()来实现的,这个很容易理解,使用方法 是: USART_Cmd(USART1, ENABLE); //使能串口 7.开启串口响应中断。有些时候当我们还需要开启串口中断,那么我们还需要使能串口中 断,使能串口中断的函数是: void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState) 这个函数的第二个入口参数是标示使能串口的类型,也就是使能哪种中断,因为串口的中断类 型有很多种。比如在接收到数据的时候(RXNE 读数据寄存器非空),我们要产生中断,那么我 们开启中断的方法是: USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启中断,接收到数据中断 我们在发送数据结束的时候(TC,发送完成)要产生中断,那么方法是: USART_ITConfig(USART1,USART_IT_TC,ENABLE); 8.获取相应中断状态。当我们使能了某个中断的时候,当该中断发生了,就会设置状态寄 存器中的某个标志位。经常我们在中断处理函数中,要判断该中断是哪种中断,使用的函数是: ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT) 比如我们使能了串口发送完成中断,那么当中断发生了, 我们便可以在中断处理函数中调用这 个函数来判断到底是否是串口发送完成中断,方法是: USART_GetITStatus(USART1, USART_IT_TC) 返回值是 SET,说明是串口发送完成中断发生。 通过以上的讲解,我们就可以达到串口最基本的配置了,关于串口更详细的介绍,请参考 《STM32 参考手册》第 516 页至 548 页,通用同步异步收发器一章。 154 8.2 硬件设计 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 本实验需要用到的硬件资源有: 1) 指示灯 DS0 2) 串口 1 串口 1 之前还没有介绍过,本实验用到的串口 1 与 USB 串口并没有在 PCB 上连接在一起, 需要通过跳线帽来连接一下。这里我们把 P4 的 RXD 和 TXD 用跳线帽与 PA9 和 PA10 连接起 来。如图 8.2.1 所示: 图 8.2.1 硬件连接图示意图 连接上这里之后,我们在硬件上就设置完成了,可以开始软件设计了。 8.3 软件设计 本章的代码设计,比前两章简单很多,因为我们的串口初始化代码和接收代码就是用我们 之前介绍的 SYSTEM 文件夹下的串口部分的内容。这里我们对代码部分稍作讲解。 打开串口实验工程,然后在 SYSTEM 组下双击 usart.c,我们就可以看到该文件里面的代码, 先介绍 uart_init 函数,该函数代码如下: //初始化 IO 串口 1 //bound:波特率 void uart_init(u32 bound) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; //①串口时钟使能,GPIO 时钟使能,复用时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1| RCC_APB2Periph_GPIOA, ENABLE); //使能 USART1,GPIOA 时钟 //②串口复位 USART_DeInit(USART1); //复位串口 1 //③GPIO 端口模式设置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //ISART1_TX PA.9 155 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); //复用推挽输出 //初始化 GPIOA.9 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; //USART1_RX PA.10 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.10 //④串口参数初始化 USART_InitStructure.USART_BaudRate = bound; //波特率设置 USART_InitStructure.USART_WordLength = USART_WordLength_8b; //字长为 8 位 USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; //一个停止位 //无奇偶校验位 USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制 USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//收发模式 USART_Init(USART1, &USART_InitStructure); //初始化串口 #if EN_USART1_RX //如果使能了接收 //⑤初始化 NVIC NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //抢占优先级 3 //子优先级 3 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); //⑤开启中断 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //IRQ 通道使能 //中断优先级初始化 //开启中断 #endif //⑥使能串口 USART_Cmd(USART1, ENABLE); //使能串口 } 从该代码可以看出,其初始化串口的过程,和我们前面介绍的一致。我们用标号①~⑥标 示了顺序: ① 串口时钟使能,GPIO 时钟使能 ② 串口复位 ③ GPIO 端口模式设置 ④ 串口参数初始化 ⑤ 初始化 NVIC 并且开启中断 ⑥ 使能串口 这里需要重申的是,对于复用功能下的 GPIO 模式怎么判定,这个需要查看《中文参 考手册 V10》P110 的表格“8.1.11 外设的 GPIO 配置”,这个我们在前面的端口复用章节有 提到,这里 还是拿出来再讲解一下吧。查看手册得知,配置全双工的串口 1,那么 TX(PA9) 管脚需要配置为推挽复用输出,RX(PA10)管脚配置为浮空输入或者带上拉输入。模式配置 参考下面表格: 156 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 表 9.3.1 串口 GPIO 模式配置表 对于 NVIC 中断优先级管理,我们在前面的章节(4.5 中断优先级管理)也有讲解,这里不 做重复讲解了。 这 里 需要 注意 一点 , 因为 我 们使 用到 了串 口 的中 断 接收 ,必 须在 usart.h 里 面设置 EN_USART1_RX 为 1(默认设置就是 1 的) 。该函数才会配置中断使能,以及开启串口 1 的 NVIC 中断。这里我们把串口 1 中断放在组 2,优先级设置为组 2 里面的最低。 接下来,根据之前讲解的步骤 7,还要编写中断服务函数。串口 1 的中断服务函数 USART1_IRQHandler,在 5.3.3 已经有详细介绍了,这里我们就不再介绍了,大家可以翻过去 看看。 从该代码可以看出,其初始化串口的过程,和我们前面介绍的一致。先计算得到 USART1->BRR 的内容。然后开始初始化串口引脚,接着把 USART1 复位,然之后设置波特率 和奇偶校验等。 这 里 需要 注意 一点 , 因为 我 们使 用到 了串 口 的中 断 接收 ,必 须在 usart.h 里 面设置 EN_USART1_RX 为 1(默认设置就是 1 的) 。该函数才会配置中断使能,以及开启串口 1 的 NVIC 中断。这里我们把串口 1 中断放在组 2,优先级设置为组 2 里面的最低。 串口 1 的中断服务函数 USART1_IRQHandler,在 5.3.1 已经有详细介绍了,这里我们就不 再介绍了。 介绍完了这两个函数,我们回到 test.c,在 test.c 里面编写如下代码: #include "sys.h" #include "usart.h" #include "delay.h" #include "led.h" int main(void) { u8 t; u8 len; u16 times=0; delay_init(); //延时函数初始化 NVIC_Configuration(); //设置中断分组 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 while(1) { if(USART_RX_STA&0x8000) { len=USART_RX_STA&0x3fff;//得到此次接收到的数据长度 printf("\r\n 您发送的消息为:\r\n"); 157 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 for(t=0;tDR=USART_RX_BUF[t]; while((USART1->SR&0X40)==0);//等待发送结束 } printf("\r\n\r\n");//插入换行 USART_RX_STA=0; }else { times++; if(times%5000==0) { printf("\r\nALIENTEK MiniSTM32 开发板 串口实验\r\n"); printf("正点原子@ALIENTEK\r\n\r\n\r\n"); } if(times%200==0)printf("请输入数据,以回车键结束\r\n"); if(times%30==0)LED0=!LED0;//闪烁 LED,提示系统正在运行. delay_ms(10); } } } 这段代码比较简单,首先我们看看 NVIC_Configuration()函数,该函数是设置中断分组号为 2,也就是 2 位抢占优先级和 2 位子优先级。 现在重点看下以下两句: USART_SendData(USART1, USART_RX_BUF[t]); //向串口 1 发送数据 while(USART_GetFlagStatus(USART1,USART_FLAG_TC)!=SET); 第一句,其实就是发送一个字节到串口。第二句呢,就是我们在我们发送一个数据到串口 之后,要检测这个数据是否已经被发送完成了。USART_FLAG_TC 是宏定义的数据发送完成标 识符。 其他的代码比较简单,我们执行编译之后看看有没有错误,没有错误就可以开始仿真与调 试了。整个工程的编译结果如图 8.3.1 所示: 图 8.3.1 编译结果 可以看到,编译没有任何错误和警告,下面我们可以开始下载验证了。 158 8.4 下载验证 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 前面两章实例,我们均介绍了软件仿真,仿真的基本技巧也差不多介绍完了,接下来我们 将淡化这部分,因为代码都是经过作者检验,并且全部在 ALIENTEK MiniSTM32 开发板上验 证了的,有兴趣的朋友可以自己仿真看看。但是这里要说明几点: 1,IO 口复用的,信号在逻辑分析窗口是不能显示出来的(看不到波形),这一点请大家注 意。比如串口的输出,SPI,USB,CAN 等。你在仿真的时候在该窗口看不到任何信息。遇到 这样的情况,你就不得不准备一个逻辑分析仪,外加一个 ULINK 或者 JLINK 来做在线调试。 但一般情况,这些都是有现成的例子,不用这几个东西一般也能编出来。 2,仿真并不能代表实际情况。只能从某些方面给你一些启示,告诉你大方向,不能尽信仿 真,当然也不能完全没有仿真。比如上面 IO 口的输出,仿真的时候,其翻转速度可以达到很 快,但是实际上 STM32 的 IO 输出就达不到这个速度。 总之,我们要合理的利用仿真,也不能过于依赖仿真。当仿真解决不了了,可以试试在线 调试,在线调试一般都可以知道问题在哪个地方,但是问题要怎么解决还是得各位自己动脑筋、 找资料了。 我们把程序下载到 MiniSTM32 开发板,可以看到板子上的 DS0 开始闪烁,说明程序已经 在跑了。串口调试助手,我们用 XCOM V1.4,该软件无需安装,直接可以运行,但是需要你的 电脑安装有.NET Framework 4.0(WIN7 直接自带了)或以上版本的环境才可以,该软件的详细介 绍请看:http://www.openedv.com/posts/list/22994.htm 这个帖子。 接着我们打 XCOM V1.4,设置串口为开发板的 USB 转串口(CH340 虚拟串口,得根据你 自己的电脑选择,我的电脑是 COM3),可以看到如图 8.4.1 所示信息: 图 8.4.1 串口调试助手收到的信息 从图 8.4.1 可以看出,STM32 的串口数据发送是没问题的了。但是,因为我们在程序上面 设置了必须输入回车,串口才认可接收到的数据,所以必须在发送数据后再发送一个回车符, 这里 XCOM 提供的发送方法是通过勾选发送新行实现,如图 8.4.1,只要勾选了这个选项,每 次发送数据后,XCOM 都会自动多发一个回车(0X0D+0X0A)。设置好了发送新行,我们再在发 送区输入你想要发送的文字,然后单击发送,可以得到如图 8.4.2 所示结果: 159 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 8.4.2 发送数据后收到的数据 可以看到,我们发送的消息被发送回来了(图中圈圈内)。大家可以试试,如果不发送回车 (取消发送新行),在输入内容之后,直接按发送是什么结果。 160 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第九章 外部中断实验 这一章,我们将向大家介绍如何使用 STM32 的外部输入中断。在前面几章的学习中,我 们掌握了 STM32 的 IO 口最基本的操作。本章我们将介绍如何将 STM32 的 IO 口作为外部中断 输入,在本章中,我们将以中断的方式,实现我们在第七章所实现的功能。本章分为如下几个 部分: 9.1 STM32 外部中断简介 9.2 硬件设计 9.3 软件设计 9.4 下载验证 161 9.1 STM32 外部中断简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 STM32 的 IO 口在第六章有详细介绍,而中断管理分组管理在前面也有详细的阐述。这里 我们将介绍 STM32 外部 IO 口的中断功能,通过中断的功能,达到第八章实验的效果,即:通 过板载的 4 个按键,控制板载的两个 LED 的亮灭以及蜂鸣器的发声。 这章的代码主要分布在固件库的 stm32f10x_exti.h 和 stm32f10x_exti.c 文件中。 这里我们首先 STM32 IO 口中断的一些基础概念。STM32 的每个 IO 都可以作为外部中断 的中断输入口,这点也是 STM32 的强大之处。STM32F103 的中断控制器支持 19 个外部中断/ 事件请求。每个中断设有状态位,每个中断/事件都有独立的触发和屏蔽设置。STM32F103 的 19 个外部中断为: 线 0~15:对应外部 IO 口的输入中断。 线 16:连接到 PVD 输出。 线 17:连接到 RTC 闹钟事件。 线 18:连接到 USB 唤醒事件。 从上面可以看出,STM32 供 IO 口使用的中断线只有 16 个,但是 STM32 的 IO 口却远远不 止 16 个,那么 STM32 是怎么把 16 个中断线和 IO 口一一对应起来的呢?于是 STM32 就这样 设计,GPIO 的管教 GPIOx.0~GPIOx.15(x=A,B,C,D,E,F,G)分别对应中断线 15~0。这样每个中 断线对应了最多 7 个 IO 口,以线 0 为例:它对应了 GPIOA.0、GPIOB.0、GPIOC.0、GPIOD.0、 GPIOE.0、GPIOF.0、GPIOG.0。而中断线每次只能连接到 1 个 IO 口上,这样就需要通过配置 来决定对应的中断线配置到哪个 GPIO 上了。下面我们看看 GPIO 跟中断线的映射关系图: 图 10.1.1 GPIO 和中断线的映射关系图 162 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 在库函数中,配置 GPIO 与中断线的映射关系的函数 GPIO_EXTILineConfig()来实现的: void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource) 该函数将 GPIO 端口与中断线映射起来,使用范例是: GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2); 将中断线 2 与 GPIOE 映射起来,那么很显然是 GPIOE.2 与 EXTI2 中断线连接了。设置好中断 线映射之后,那么到底来自这个 IO 口的中断是通过什么方式触发的呢?接下来我们就要设置 该中断线上中断的初始化参数了。 中断线上中断的初始化是通过函数 EXTI_Init()实现的。EXTI_Init()函数的定义是: void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); 下面我们用一个使用范例来说明这个函数的使用: EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line=EXTI_Line4; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); //根据 EXTI_InitStruct 中指定的 //参数初始化外设 EXTI 寄存器 上面的例子设置中断线 4 上的中断为下降沿触发。STM32 的外设的初始化都是通过结构体来设 置初始值的,这里就不罗嗦结构体初始化的过程了。我们来看看结构体 EXTI_InitTypeDef 的成 员变量: typedef struct { uint32_t EXTI_Line; EXTIMode_TypeDef EXTI_Mode; EXTITrigger_TypeDef EXTI_Trigger; FunctionalState EXTI_LineCmd; }EXTI_InitTypeDef; 从定义可以看出,有 4 个参数需要设置。第一个参数是中断线的标号,取值范围为 EXTI_Line0~EXTI_Line15。这个在上面已经讲过中断线的概念。也就是说,这个函数配置的是 某个中断线上的中断参数。第二个参数是中断模式,可选值为中断 EXTI_Mode_Interrupt 和事 件 EXTI_Mode_Event。第三个参数是触发方式,可以是下降沿触发 EXTI_Trigger_Falling,上 升沿触发 EXTI_Trigger_Rising,或者任意电平(上升沿和下降沿)触发 EXTI_Trigger_Rising_Falling,相信学过 51 的对这个不难理解。最后一个参数就是使能中断线 了。 我们设置好中断线和 GPIO 映射关系,然后又设置好了中断的触发模式等初始化参数。既 然是外部中断,涉及到中断我们当然还要设置 NVIC 中断优先级。这个在前面已经讲解过,这 里我们就接着上面的范例, 设置中断线 2 的中断优先级。 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2, NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级 2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //中断优先级分组初始化 163 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 上面这段代码相信大家都不陌生,我们在前面的串口实验的时候讲解过,这里不再讲解。 我们配置完中断优先级之后,接着我们要做的就是编写中断服务函数。中断服务函数的名 字是在 MDK 中事先有定义的。这里需要说明一下,STM32 的 IO 口外部中断函数只有 6 个, 分别为: EXPORT EXTI0_IRQHandler EXPORT EXTI1_IRQHandler EXPORT EXTI2_IRQHandler EXPORT EXTI3_IRQHandler EXPORT EXTI4_IRQHandler EXPORT EXTI9_5_IRQHandler EXPORT EXTI15_10_IRQHandler 中断线 0-4 每个中断线对应一个中断函数,中断线 5-9 共用中断函数 EXTI9_5_IRQHandler,中 断线 10-15 共用中断函数 EXTI15_10_IRQHandler。在编写中断服务函数的时候会经常使用到两 个函数,第一个函数是判断某个中断线上的中断是否发生(标志位是否置位): ITStatus EXTI_GetITStatus(uint32_t EXTI_Line); 这个函数一般使用在中断服务函数的开头判断中断是否发生。另一个函数是清除某个中断线上 的中断标志位: void EXTI_ClearITPendingBit(uint32_t EXTI_Line); 这个函数一般应用在中断服务函数结束之前,清除中断标志位。 常用的中断服务函数格式为: void EXTI2_IRQHandler(void) { if(EXTI_GetITStatus(EXTI_Line3)!=RESET)//判断某个线上的中断是否发生 { 中断逻辑… EXTI_ClearITPendingBit(EXTI_Line3); //清除 LINE 上的中断标志位 } } 在这里需要说明一下,固件库还提供了两个函数用来判断外部中断状态以及清除外部状态 标志位的函数 EXTI_GetFlagStatus 和 EXTI_ClearFlag,他们的作用和前面两个函数的作用类似。 只是在 EXTI_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而 EXTI_GetFlagStatus 直接用来判断状态标志位。 讲到这里,相信大家对于 STM32 的 IO 口外部中断已经有了一定了了解。下面我们再总结一 下使用 IO 口外部中断的一般步骤: 1)初始化 IO 口为输入。 2)开启 IO 口复用时钟,设置 IO 口与中断线的映射关系。 3)初始化线上中断,设置触发条件等。 4)配置中断分组(NVIC),并使能中断。 5)编写中断服务函数。 通过以上几个步骤的设置,我们就可以正常使用外部中断了。 本章,我们要实现同第八章差不多的功能,但是这里我们使用的是中断来检测按键,还是 WK_UP 控制蜂鸣器,按一次叫,再按一次停;KEY2 控制 DS0,按一次亮,再按一次灭;KEY1 控制 DS1,效果同 KEY2;KEY0 则同时控制 DS0 和 DS1,按一次,他们的状态就翻转一次。 164 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 9.2 硬件设计 本实验用到的硬件资源和第七章实验的一模一样,不再多做介绍了。 9.3 软件设计 软件设计我们直接打开我们的光盘的实验 4 的工程,可以看到相比上一个工程,我们的 HARDWARE 目录下面增加了 exti.c 文件,同时固件库目录增加了 stm32f10x_exti.c 文件。 exit.c 文件总共包含 4 个函数。一个是外部中断初始化函数 void EXTIX_Init(void),另外 3 个都是中断服务函数。 void EXTI0_IRQHandler(void)是外部中断 0 的服务函数,负责 WK_UP 按键的中断检测; void EXTI9_5_IRQHandler (void)是外部中断 5~9 的服务函数,负责 KEY0 按键的中断检测; void EXTI15_10_IRQHandler (void)是外部中断 10~15 的服务函数,负责 KEY1 按键的中断 检测; exti.c 的代码如下: #include "exti.h" #include "led.h" #include "key.h" #include "delay.h" #include "usart.h" //外部中断初始化函数 void EXTIX_Init(void) { EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //外部中断,需要使能 AFIO 时钟 KEY_Init();//初始化按键对应 io 模式 //GPIOC.5 中断线以及中断初始化配置 GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource5); EXTI_InitStructure.EXTI_Line=EXTI_Line5; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//下降沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); //根据 EXTI_InitStruct 中指定的参数初始化外设 EXTI 寄存器 //GPIOA.15 中断线以及中断初始化配置 165 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15); EXTI_InitStructure.EXTI_Line=EXTI_Line15; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); //根据 EXTI_InitStruct 中指定的参数初始化外设 EXTI 寄存器 //GPIOA.0 中断线以及中断初始化配置 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0); EXTI_InitStructure.EXTI_Line=EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); //根据 EXTI_InitStruct 中指定的参数初始化外设 EXTI 寄存器 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键所在的外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级 1 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //根据 NVIC_InitStruct 中指定的参数初始化外设 NVIC 寄存器 NVIC_InitStructure.NVIC_IRQChannel = EXTI9_5_IRQn; //使能按键所在的外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2, NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级 1 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn; //使能按键所在的外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级 2, NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //子优先级 1 //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); 166 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 } void EXTI0_IRQHandler(void) { delay_ms(10); //消抖 if(WK_UP==1) { LED0=!LED0; LED1=!LED1; } EXTI_ClearITPendingBit(EXTI_Line0); //清除 EXTI0 线路挂起位 } void EXTI9_5_IRQHandler(void) { delay_ms(10); //消抖 if(KEY0==0) { LED0=!LED0; } EXTI_ClearITPendingBit(EXTI_Line5); //清除 LINE5 上的中断标志位 } void EXTI15_10_IRQHandler(void) { delay_ms(10); //消抖 if(KEY1==0) { LED1=!LED1; } EXTI_ClearITPendingBit(EXTI_Line15); //清除 LINE15 线路挂起位 } exti.c 文件总共包含 4 个函数。一个是外部中断初始化函数 void EXTI_Init(void),另外 3 个 都是中断服务函数。void EXTI0_IRQHandler(void)是外部中断 0 的服务函数,负责 WK_UP 按 键的中断检测;void EXTI9_5_IRQHandler(void)是外部中断 9~5 的服务函数,负责 KEY0 按键 的中断检测; void EXTI15_10_IRQHandler(void)是外部中断 15~10 的服务函数,负责 KEY1 按 键的中断检测;下面我们分别介绍这几个函数。 首先是外部中断初始化函数 void EXTI_Init(void),该函数严格按照我们之前的步骤来初始 化外部中断,首先调用 KEY_Init 函数(第七章有介绍),来初始化外部中断输入的 IO 口,接着 调用 RCC_APB2PeriphClockCmd()函数来使能复用功能时钟。接着配置中断线和 GPIO 的映射 关系,然后初始化中断线。需要说明的是因为我们的 WK_UP 按键是高电平有效的,而 KEY0 和 KEY1 是低电平有效的,所以我们设置 WK_UP 为上升沿触发中断,而 KEY0 和 KEY1 则设 置为下降沿触发。这里我们把所有中断都分配到第二组,把按键的抢占优先级设置成一样,而 子优先级不同,这三个按键,KEY1 的优先级最高。 接下来我们介绍各个按键的中断服务函数,一共 3 个。先看 WK_UP 的中断服务函数 void 167 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 EXTI0_IRQHandler(void),该函数代码比较简单,先延时 10ms 以消抖,再检测 WK_UP 是否还 是为高电平,如果是,则执行此次操作(DS0&DS1 取反),如果不是,则直接跳过,在最后有 一句 EXTI_ClearITPendingBit(EXTI_Line2);通过该句清除已经发生的中断请求。同样,我们可 以发现 KEY0 和 KEY1 的中断服务函数和 WK_UP 按键的十分相似,我们就不逐个介绍了。 这里向大家说明一下,STM32 的外部中断 0~4 都有单独的中断服务函数,但是从 5 开始, 他们就没有单独的服务函数了,而是多个中断共用一个服务函数,比如外部中断 5~9 的中断服 务函数为:void EXTI9_5_IRQHandler(void),类似的,void EXTI15_10_IRQHandler(void)就是 外部中断 10~15 的中断服务函数。 文件 exti.h 内容很简单,只有一个 EXTIX_Init 函数初始化各个外部中断。 接下来我们看看 main.c 里面里面的内容: #include "led.h" #include "delay.h" #include "sys.h" #include "key.h" #include "usart.h" #include "exti.h" //ALIENTEK Mini STM32 开发板范例代码 4 //外部中断实验 //技术支持:www.openedv.com //广州市星翼电子科技有限公司 int main(void) { delay_init(); //延时函数初始化 NVIC_Configuration(); //设置中断分组 uart_init(9600); //串口初始化波特率为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 EXTIX_Init(); //外部中断初始化 LED0=0; //点亮 LED while(1) { printf("OK\n"); delay_ms(1000); } } 该部分代码很简单,在初始化完中断后,点亮 LED0,就进入死循环等待了,这里死循环 里面通过一个 printf 函数来告诉我们系统正在运行,在中断发生后,就执行相应的处理,从而 实现第七章类似的功能。 9.4 下载验证 在编译成功之后,我们就可以下载代码到 MiniSTM32 开发板上,实际验证一下我们的程序 是否正确。下载代码后,在串口调试助手里面可以看到如图 9.4.1 所示信息: 168 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 9.4.1 串口收到的数据 从图 9.4.1 可以看出,程序已经在运行了,此时可以通过按下 KEY0、KEY1 和 WK_UP 来 观察 DS0 和 DS1 是否跟着按键的变化而变化。 169 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十章 独立看门狗(IWDG)实验 这一章,我们将向大家介绍如何使用 STM32 的独立看门狗(以下简称 IWDG)。STM32 内 部自带了 2 个看门狗:独立看门狗(IWDG)和窗口看门狗(WWDG)。这一章我们只介绍独 立看门狗,窗口看门狗将在下一章介绍。在本章中,我们将通过按键 WK_UP 来喂狗,然后通 过 DS0 提示复位状态。本章分为如下几个部分: 10.1 STM32 独立看门狗简介 10.2 硬件设计 10.3 软件设计 10.4 下载验证 170 10.1 STM32 独立看门狗简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 STM32 的独立看门狗由内部专门的 40Khz 低速时钟驱动,即使主时钟发生故障,它也仍然 有效。这里需要注意独立看门狗的时钟是一个内部 RC 时钟,所以并不是准确的 40Khz,而是 在 30~60Khz 之间的一个可变化的时钟,只是我们在估算的时候,以 40Khz 的频率来计算,看 门狗对时间的要求不是很精确,所以,时钟有些偏差,都是可以接受的。 独立看门狗有几个寄存器与我们这节相关,我们分别介绍这几个寄存器,首先是键值寄存 器 IWDG_KR,该寄存器的各位描述如图 10.1.1 所示: 图 10.1.1 IWDG_KR 寄存器各位描述 在键寄存器(IWDG_KR)中写入 0xCCCC,开始启用独立看门狗;此时计数器开始从其复位 值 0xFFF 递减计数。当计数器计数到末尾 0x000 时,会产生一个复位信号(IWDG_RESET)。 无 论何时,只要键寄存器 IWDG_KR 中被写入 0xAAAA, IWDG_RLR 中的值就会被重新加载到 计数器中从而避免产生看门狗复位 。 IWDG_PR 和 IWDG_RLR 寄存器具有写保护功能。要修改这两个寄存器的值,必须先向 IWDG_KR 寄存器中写入 0x5555。将其他值写入这个寄存器将会打乱操作顺序,寄存器将重新 被保护。重装载操作(即写入 0xAAAA)也会启动写保护功能。 接下来,我们介绍预分频寄存器(IWDG_PR),该寄存器用来设置看门狗时钟的分频系数, 最低为 4,最高位 256,该寄存器是一个 32 位的寄存器,但是我们只用了最低 3 位,其他都是 保留位。预分频寄存器各位定义如图 10.1.2 所示: 171 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 10.1.2 IWDG_ PR 寄存器各位描述 在介绍完 IWDG_PR 之后,我们介绍一下重装载寄存器。该寄存器用来保存重装载到计数 器中的值。该寄存器也是一个 32 位寄存器,但是只有低 12 位是有效的,该寄存器的各位描述 如图 10.1.3 所示: 图 10.1.3 重装载寄存器各位描述 只要对以上三个寄存器进行相应的设置,我们就可以启动 STM32 的独立看门狗,通过对 寄存器的讲解我们大致了解了独立看门狗原理和配置方法。接下来我们通过库函数方法教大家 一步一步配置独立看门狗。独立看门狗相关的库函数和定义分布在文件 stm32f10x_iwdg.h 和 stm32f10x_iwdg.c 中。 1)取消寄存器写保护(向 IWDG_KR 写入 0X5555) 通过这步,我们取消 IWDG_PR 和 IWDG_RLR 的写保护,使后面可以操作这两个寄存器, 设置 IWDG_PR 和 IWDG_RLR 的值。这在库函数中的实现函数是: IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); 这个函数非常简单,顾名思义就是开启/取消写保护,也就是使能/失能写权限。 2)设置独立看门狗的预分频系数和重装载值 设置看门狗的分频系数的函数是: void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); //设置 IWDG 预分频值 设置看门狗的重装载值的函数是: void IWDG_SetReload(uint16_t Reload); //设置 IWDG 重装载值 设置好看门狗的分频系数 prer 和重装载值就可以知道看门狗的喂狗时间(也就是看门狗溢 出时间),该时间的计算方式为: Tout=((4×2^prer) ×rlr) /40 其中 Tout 为看门狗溢出时间(单位为 ms);prer 为看门狗时钟预分频值(IWDG_PR 值), 范围为 0~7;rlr 为看门狗的重装载值(IWDG_RLR 的值); 比如我们设定 prer 值为 4,rlr 值为 625,那么就可以得到 Tout=64×625/40=1000ms,这样, 看门狗的溢出时间就是 1s,只要你在一秒钟之内,有一次写入 0XAAAA 到 IWDG_KR,就不 会导致看门狗复位(当然写入多次也是可以的)。这里需要提醒大家的是,看门狗的时钟不是准 确的 40Khz,所以在喂狗的时候,最好不要太晚了,否则,有可能发生看门狗复位。 3)重载计数值喂狗(向 IWDG_KR 写入 0XAAAA) 库函数里面重载计数值的函数是: 172 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 IWDG_ReloadCounter(); //按照 IWDG 重装载寄存器的值重装载 IWDG 计数器 通过这句,将使 STM32 重新加载 IWDG_RLR 的值到看门狗计数器里面。即实现独立看门 狗的喂狗操作。 4) 启动看门狗(向 IWDG_KR 写入 0XCCCC) 库函数里面启动独立看门狗的函数是: IWDG_Enable(); //使能 IWDG 通过这句,来启动 STM32 的看门狗。注意 IWDG 在一旦启用,就不能再被关闭!想要关 闭,只能重启,并且重启之后不能打开 IWDG,否则问题依旧,所以在这里提醒大家,如果不 用 IWDG 的话,就不要去打开它,免得麻烦。 通过上面 4 个步骤,我们就可以启动 STM32 的看门狗了,使能了看门狗,在程序里面就 必须间隔一定时间喂狗,否则将导致程序复位。利用这一点,我们本章将通过一个 LED 灯来指 示程序是否重启,来验证 STM32 的独立看门狗。 在配置看门狗后,DS0 将常亮,如果 WK_UP 按键按下,就喂狗,只要 WK_UP 不停的按, 看门狗就一直不会产生复位,保持 DS0 的常亮,一旦超过看门狗定溢出时间(Tout)还没按, 那么将会导致程序重启,这将导致 DS0 熄灭一次。 10.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 按键 3) 独立看门狗 前面两个在之前都有介绍,而独立看门狗实验的核心是在 STM32 内部进行,并不需要外 部电路。但是考虑到指示当前状态和喂狗等操作,我们需要 2 个 IO 口,一个用来输入喂狗信 号,另外一个用来指示程序是否重启。喂狗我们采用板上的 WK_UP 键来操作,而程序重启, 则是通过 DS0 来指示的。 10.3 软件设计 我们直接打开光盘的独立看门狗实验工程,可以看到工程里面新增了 wdg.c,同时引入了 头 文 件 wdg.h 。 同 样 的 道 理 , 我 们 要 加 入 固 件 库 看 门 狗 支 持 文 件 stm32f10x_iwdg.h 和 stm32f10x_iwdg.c 文件。 wdg.c 里面的代码如下: #include "wdg.h" //初始化独立看门狗 //prer:分频数:0~7(只有低 3 位有效!) //分频因子=4*2^prer.但最大值只能是 256! //rlr:重装载寄存器值:低 11 位有效. //时间计算(大概):Tout=((4*2^prer)*rlr)/40 (ms). void IWDG_Init(u8 prer,u16 rlr) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); //使能对寄存器 IWDG_PR 和 IWDG_RLR 的写操作 IWDG_SetPrescaler(prer); //设置 IWDG 预分频值:设置 IWDG 预分频值为 64 173 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 IWDG_SetReload(rlr); //设置 IWDG 重装载值 IWDG_ReloadCounter(); //按照 IWDG 重装载寄存器的值重装载 IWDG 计数器 IWDG_Enable(); //使能 IWDG} //喂独立看门狗 void IWDG_Feed(void) { IWDG->KR=0XAAAA;//reload } 该代码就 2 个函数,void IWDG_Init(u8 prer,u16 rlr)是独立看门狗初始化函数,就是按照 上面介绍的步骤来初始化独立看门狗的。该函数有 2 个参数,分别用来设置与预分频数与重装 寄存器的值的。通过这两个参数,就可以大概知道看门狗复位的时间周期为多少了。其计算方 式上面有详细的介绍,这里不再多说了。 void IWDG_Feed(void)函数,该函数用来喂狗,因为 STM32 的喂狗只需要向键值寄存器写 入 0XAAAA 即可,所以,我们这个函数也是简单的很。 头文件 wdg.h 的源码如下大家可以看下,主要是两个函数的申明。这里我们就不列出来了。 接下来我们看看主函数 main 的代码。在主程序里面我们先初始化一下系统代码,然后启动 按键输入和看门狗,在看门狗开启后马山点亮 LED0(DS0),并进入死循环等待按键的输入, 一旦 WK_UP 有按键,则喂狗,否则等待 IWDG 复位的到来。这段代码很容易理解,该部分代 码如下: int main(void) { Stm32_Clock_Init(9); //系统时钟设置 delay_init(72); //延时初始化 uart_init(72,9600); //串口初始化 LED_Init(); //初始化与 LED 连接的硬件接口 KEY_Init(); //按键初始化 delay_ms(300); //让人看得到灭 IWDG_Init(4,625); LED0=0; //与分频数为 64,重载值为 625,溢出时间为 1s //点亮 LED0 while(1) { if(KEY_Scan(0)==WKUP_PRES)IWDG_Feed();//如果 WK_UP 按下,则喂狗 delay_ms(10); }; } 上面的代码,鉴于篇幅考虑,我们没有把头文件给列出来(后续实例将会采用类同的方式 处理),因为以后我们包含的头文件会越来越多,大家想看,可以直接打开光盘相关源码查看。 至此,独立看门狗的实验代码,我们就全部编写完了,接着要做的就是下载验证了,看看我们 的代码是否真的正确,当然在下载之前可以通过软件仿真看看是否可行。 10.4 下载验证 在编译成功之后,我们就可以下载代码到 MiniSTM32 开发板上,实际验证一下,我们的程 174 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 序是否正确。下载代码后,可以看到 DS0 不停的闪烁,证明程序在不停的复位,否则只会 DS0 常亮。这时我们试试不停的按 WK_UP 按键,可以看到 DS0 就常亮了,不会再闪烁。说明我们 的实验是成功的。 175 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十一章 窗口门狗(WWDG)实验 这一章,我们将向大家介绍如何使用 STM32 的另外一个看门狗,窗口看门狗(以下简称 WWDG)。在本章中,我们将使用窗口看门狗的中断功能来喂狗,通过 DS0 和 DS1 提示程序的 运行状态。本章分为如下几个部分: 11.1 STM32 窗口看门狗简介 11.2 硬件设计 11.3 软件设计 11.4 下载验证 176 11.1 STM32 窗口看门狗简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 窗口看门狗(WWDG)通常被用来监测由外部干扰或不可预见的逻辑条件造成的应用程序 背离正常的运行序列而产生的软件故障。除非递减计数器的值在 T6 位(WWDG->CR 的第六位) 变成 0 前被刷新,看门狗电路在达到预置的时间周期时,会产生一个 MCU 复位。在递减计数 器达到窗口配置寄存器(WWDG->CFR)数值之前,如果 7 位的递减计数器数值(在控制寄存器中) 被刷新, 那么也将产生一个 MCU 复位。这表明递减计数器需要在一个有限的时间窗口中被刷 新。他们的关系可以用图 11.1.1 来说明: 图 11.1.1 窗口看门狗工作示意图 图 11.1.1 中,T[6:0]就是 WWDG_CR 的低七位,W[6:0]即是 WWDG->CFR 的低七位。T[6:0] 就是窗口看门狗的计数器,而 W[6:0]则是窗口看门狗的上窗口,下窗口值是固定的(0X40)。 当窗口看门狗的计数器在上窗口值之外被刷新,或者低于下窗口值都会产生复位。 上窗口值(W[6:0])是由用户自己设定的,根据实际要求来设计窗口值,但是一定要确保 窗口值大于 0X40,否则窗口就不存在了。 窗口看门狗的超时公式如下: Twwdg=(4096×2^WDGTB×(T[5:0]+1)) /Fpclk1; 其中: Twwdg:WWDG 超时时间(单位为 ms) Fpclk1:APB1 的时钟频率(单位为 Khz) WDGTB:WWDG 的预分频系数 T[5:0]:窗口看门狗的计数器低 6 位 根据上面的公式,假设 Fpclk1=36Mhz,那么可以得到最小-最大超时时间表如表 11.1.1 所 示: 177 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 表 11.1.1 36M 时钟下窗口看门狗的最小最大超时表 接下来,我们介绍窗口看门狗的 3 个寄存器。首先介绍控制寄存器(WWDG_CR),该寄 存器的各位描述如图 11.1.2 所示: 图 11.1.2 WWDG_CR 寄存器各位描述 可以看出,这里我们的 WWDG_CR 只有低八位有效,T[6:0]用来存储看门狗的计数器值, 随时更新的,每个窗口看门狗计数周期(4096×2^ WDGTB)减 1。当该计数器的值从 0X40 变 为 0X3F 的时候,将产生看门狗复位。 WDGA 位则是看门狗的激活位,该位由软件置 1,以启动看门狗,并且一定要注意的是该 位一旦设置,就只能在硬件复位后才能清零了。 窗口看门狗的第二个寄存器是配置寄存器(WWDG_CFR),该寄存器的各位及其描述如图 11.1.3 所示: 图 11.1.3 WWDG_ CFR 寄存器各位描述 该位中的 EWI 是提前唤醒中断,也就是在快要产生复位的前一段时间(T[6:0]=0X40)来 提醒我们,需要进行喂狗了,否则将复位!因此,我们一般用该位来设置中断,当窗口看门狗 的计数器值减到 0X40 的时候,如果该位设置,并开启了中断,则会产生中断,我们可以在中 断里面向 WWDG_CR 重新写入计数器的值,来达到喂狗的目的。注意这里在进入中断后,必 须在不大于 1 个窗口看门狗计数周期的时间(在 PCLK1 频率为 36M 且 WDGTB 为 0 的条件下, 该时间为 113us)内重新写 WWDG_CR,否则,看门狗将产生复位! 最后我们要介绍的是状态寄存器(WWDG_SR),该寄存器用来记录当前是否有提前唤醒 的标志。该寄存器仅有位 0 有效,其他都是保留位。当计数器值达到 40h 时,此位由硬件置 1。 它必须通过软件写 0 来清除。对此位写 1 无效。即使中断未被使能,在计数器的值达到 0X40 178 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 的时候,此位也会被置 1。 在介绍完了窗口看门狗的寄存器之后,我们介绍要如何启用 STM32 的窗口看门狗。这里 我们介绍库函数中用中断的方式来喂狗的方法,窗口看门狗库函数相关源码和定义分布在文件 stm32f10x_wwdg.c 文件和头文件 stm32f10x_wwdg.h 中。步骤如下: 1)使能 WWDG 时钟 WWDG 不同于 IWDG,IWDG 有自己独立的 40Khz 时钟,不存在使能问题。而 WWDG 使用的是 PCLK1 的时钟,需要先使能时钟。方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG 时钟使能 2)设置窗口值和分频数 设置窗口值的函数是: void WWDG_SetWindowValue(uint8_t WindowValue); 这个函数就一个入口参数为窗口值,很容易理解。 设置分频数的函数是: void WWDG_SetPrescaler(uint32_t WWDG_Prescaler); 这个函数同样只有一个入口参数就是分频值。 3)开启 WWDG 中断并分组 开启 WWDG 中断的函数为: WWDG_EnableIT(); //开启窗口看门狗中断 接下来是进行中断优先级配置,这里就不重复了,使用 NVIC_Init()函数即可。 4)设置计数器初始值并使能看门狗 这一步在库函数里面是通过一个函数实现的: void WWDG_Enable(uint8_t Counter); 该函数既设置了计数器初始值,同时使能了窗口看门狗。 5)编写中断服务函数 在最后,还是要编写窗口看门狗的中断服务函数,通过该函数来喂狗,喂狗要快,否则当 窗口看门狗计数器值减到 0X3F 的时候,就会引起软复位了。在中断服务函数里面也要将状态 寄存器的 EWIF 位清空。 完成了以上 5 个步骤之后,我们就可以使用 STM32 的窗口看门狗了。这一章的实验,我 们将通过 DS0 来指示 STM32 是否被复位了,如果被复位了就会点亮 300ms。DS1 用来指示中 断喂狗,每次中断喂狗翻转一次。 11.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 窗口看门狗 其中指示灯前面介绍过了,窗口看门狗属于 STM32 的内部资源,只需要软件设置好即可 正常工作。我们通过 DS0 和 DS1 来指示 STM32 的复位情况和窗口看门狗的喂狗情况。 11.3 软件设计 打开我们的窗口看门狗实验可以看到,相对于独立看门狗,我们只增加了窗口看门狗相关 的库函数支持文件 stm32f10x_wwdg.c/stm32f10x_wwdg./c,同时新加了 wwdg.c 源文件和引入了 对应的头文件 wwdg.h。wwdg.c 源文件内容如下 #include "wwdg.h" 179 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #include "led.h" ////////////////////////////////////////////////////////////////////////////////// //本程序只供学习使用,未经作者许可,不得用于其它任何用途 //Mini STM32 开发板 //看门狗 驱动代码 //正点原子@ALIENTEK //技术论坛:www.openedv.com //修改日期:2010/5/30 //版本:V1.0 //版权所有,盗版必究。 //Copyright(C) 正点原子 2009-2019 //All rights reserved //保存 WWDG 计数器的设置值,默认为最大. u8 WWDG_CNT=0x7f; void WWDG_Init(u8 tr,u8 wr,u32 fprer) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG 时钟使能 WWDG_SetPrescaler(fprer);////设置 IWDG 预分频值 WWDG_SetWindowValue(wr);//设置窗口值 WWDG_Enable(tr); //使能看门狗 , 设置 counter . WWDG_ClearFlag(); WWDG_NVIC_Init();//初始化窗口看门狗 NVIC WWDG_EnableIT(); //开启窗口看门狗中断 } //重设置 WWDG 计数器的值 void WWDG_Set_Counter(u8 cnt) { WWDG_Enable(cnt); } //窗口看门狗中断服务程序 void WWDG_NVIC_Init() { NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn; //WWDG 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占 2 子优先级 3 组 2 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //抢占 2,子优先级 3,组 2 NVIC_Init(&NVIC_InitStructure);//NVIC 初始化 } 180 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 void WWDG_IRQHandler(void) { // Update WWDG counter WWDG_SetCounter(0x7F); //当禁掉此句后,窗口看门狗将产生复位 // Clear EWI flag */ WWDG_ClearFlag(); //清除提前唤醒中断标志位 // Toggle GPIO_Led pin 7 */ LED1=!LED1; } wwdg.c 文件里面三个函数都比较简单,第一个函数 void WWDG_Init(u8 tr,u8 wr,u8 fprer) 用来设置 WWDG 的初始化值。包括看门狗计数器的值和看门狗比较值等。该函数就是按照我 们上面的 4 个思路设计出来的代码。注意到这里有个全局变量 WWDG_CNT,该变量用来保存 最初设置 WWDG_CR 计数器的值。在后续的中断服务函数里面,就又把该数值放回到 WWDG_CR 上。 WWDG_Set_Counter 函数比较简单,就是用来重设窗口看门狗的计数器值的。该函数很简 单,我们就不多说了。 最后在中断服务函数里面,先重设窗口看门狗的计数器值,然后清除提前唤醒中断标志。 最后对 LED1(DS1)取反,来监测中断服务函数的执行了状况。我们再把这几个函数名加入到 头文件里面去,以方便其他文件调用。 在完成了以上部分之后,我们就回到主函数,代码如下: #include "led.h" #include "delay.h" #include "sys.h" #include "usart.h" #include "wwdg.h" //ALIENTEK Mini STM32 开发板范例代码 6 //窗口看门狗实验 //技术支持:www.openedv.com //广州市星翼电子科技有限公司 int main(void) { delay_init(); NVIC_Configuration(); uart_init(9600); LED_Init(); LED0=0; delay_ms(300); WWDG_Init(0X7F,0X5F,WWDG_Prescaler_8); while(1) { 181 } } LED0=1; STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 该函数通过 LED0(DS0)来指示是否正在初始化。而 LED1(DS1)用来指示是否发生了中断。 我们先让 LED0 亮 300ms,然后关闭以用于判断是否有复位发生了。在初始化 WWDG 之后, 我们回到死循环,关闭 LED1,并等待看门狗中断的触发/复位。 在编译完成之后,我们就可以下载这个程序到 MiniSTM32 开发板上,看看结果是不是和我 们设计的一样。 11.4 下载验证 将代码下载到 MiniSTM32 后,可以看到 DS0 亮一下之后熄灭,紧接着 DS1 开始不停的闪 烁。每秒钟闪烁 9 次左右,和我们预期的一致,说明我们的实验是成功的。 182 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十二章 定时器中断实验 这一章,我们将向大家介绍如何使用 STM32 的通用定时器,STM32 的定时器功能十分强 大,有 TIME1 和 TIME8 等高级定时器,也有 TIME2~TIME5 等通用定时器,还有 TIME6 和 TIME7 等基本定时器。在《STM32 参考手册》里面,定时器的介绍占了 1/5 的篇幅,足见其重 要性。在本章中,我们将使用 TIM3 的定时器中断来控制 DS1 的翻转,在主函数用 DS0 的翻转 来提示程序正在运行。本章,我们选择难度适中的通用定时器来介绍,本章将分为如下几个部 分: 12.1 STM32 通用定时器简介 12.2 硬件设计 12.3 软件设计 12.4 下载验证 183 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 12.1 STM32 通用定时器简介 STM32 的通用定时器是一个通过可编程预分频器(PSC)驱动的 16 位自动装载计数器(CNT) 构成。STM32 的通用定时器可以被用于:测量输入信号的脉冲长度(输入捕获)或者产生输出波 形(输出比较和 PWM)等。 使用定时器预分频器和 RCC 时钟控制器预分频器,脉冲长度和波形 周期可以在几个微秒到几个毫秒间调整。STM32 的每个通用定时器都是完全独立的,没有互相 共享的任何资源。 STM3 的通用 TIMx (TIM2、TIM3、TIM4 和 TIM5)定时器功能包括: 1)16 位向上、向下、向上/向下自动装载计数器(TIMx_CNT)。 2)16 位可编程(可以实时修改)预分频器(TIMx_PSC),计数器时钟频率的分频系数为 1~ 65535 之间的任意数值。 3)4 个独立通道(TIMx_CH1~4),这些通道可以用来作为: A.输入捕获 B.输出比较 C.PWM 生成(边缘或中间对齐模式) D.单脉冲模式输出 4)可使用外部信号(TIMx_ETR)控制定时器和定时器互连(可以用 1 个定时器控制另外 一个定时器)的同步电路。 5)如下事件发生时产生中断/DMA: A.更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发) B.触发事件(计数器启动、停止、初始化或者由内部/外部触发计数) C.输入捕获 D.输出比较 E.支持针对定位的增量(正交)编码器和霍尔传感器电路 F.触发输入作为外部时钟或者按周期的电流管理 由于 STM32 通用定时器比较复杂,这里我们不再多介绍,请大家直接参考《STM32 参考 手册》第 253 页,通用定时器一章。下面我们介绍一下与我们这章的实验密切相关的几个通用 定时器的寄存器。 首先是控制寄存器 1(TIMx_CR1),该寄存器的各位描述如图 12.1.1 所示: 184 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 185 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 12.1.1 TIMx_CR1 寄存器各位描述 在本实验中,我们只用到了 TIMx_CR1 的最低位,也就是计数器使能位,该位必须置 1, 才能让定时器开始计数。接下来介绍第二个与我们这章密切相关的寄存器:DMA/中断使能寄 存器(TIMx_DIER)。该寄存器是一个 16 位的寄存器,其各位描述如图 12.1.2 所示: 图 12.1.2 TIMx_ DIER 寄存器各位描述 这里我们同样仅关心它的第 0 位,该位是更新中断允许位,本章用到的是定时器的更新中 断,所以该位要设置为 1,来允许由于更新事件所产生的中断。 接下来我们看第三个与我们这章有关的寄存器:预分频寄存器(TIMx_PSC)。该寄存器用 设置对时钟进行分频,然后提供给计数器,作为计数器的时钟。该寄存器的各位描述如图 12.1.3 所示: 图 12.1.3 TIMx_ PSC 寄存器各位描述 这里,定时器的时钟来源有 4 个: 1)内部时钟(CK_INT) 2)外部时钟模式 1:外部输入脚(TIx) 3)外部时钟模式 2:外部触发输入(ETR) 4)内部触发输入(ITRx):使用 A 定时器作为 B 定时器的预分频器(A 为 B 提供时钟)。 这些时钟,具体选择哪个可以通过 TIMx_SMCR 寄存器的相关位来设置。这里的 CK_INT 时钟是从 APB1 倍频的来的,STM32 中除非 APB1 的时钟分频数设置为 1,否则通用定时器 TIMx 的时钟是 APB1 时钟的 2 倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx 的时钟就等于 APB1 的时钟。这里还要注意的就是高级定时器的时钟不是来自 APB1,而是来自 APB2 的。 这里顺带介绍一下 TIMx_CNT 寄存器,该寄存器是定时器的计数器,该寄存器存储了当前 定时器的计数值。 接着我们介绍自动重装载寄存器(TIMx_ARR),该寄存器在物理上实际对应着 2 个寄存器。 一个是程序员可以直接操作的,另外一个是程序员看不到的,这个看不到的寄存器在《STM32 参考手册》里面被叫做影子寄存器。事实上真正起作用的是影子寄存器。根据 TIMx_CR1 寄存 器中 APRE 位的设置:APRE=0 时,预装载寄存器的内容可以随时传送到影子寄存器,此时 2 者是连通的;而 APRE=1 时,在每一次更新事件(UEV)时,才把预装在寄存器的内容传送到 影子寄存器。 自动重装载寄存器的各位描述如图 12.1.4 所示: 186 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 12.1.4 TIMx_ ARR 寄存器各位描述 最后,我们要介绍的寄存器是:状态寄存器(TIMx_SR)。该寄存器用来标记当前与定时 器相关的各种事件/中断是否发生。该寄存器的各位描述如图 12.1.5 所示: 图 12.1.5 TIMx_ SR 寄存器各位描述 关于这些位的详细描述,请参考《STM32 参考手册》第 282 页。 只要对以上几个寄存器进行简单的设置,我们就可以使用通用定时器了,并且可以产生中 断。 这一章,我们将使用定时器产生中断,然后在中断服务函数里面翻转 DS1 上的电平,来指 示定时器中断的产生。接下来我们以通用定时器 TIM3 为实例,来说明要经过哪些步骤,才能 达到这个要求,并产生中断。这里我们就对每个步骤通过库函数的实现方式来描述。首先要提 到的是,定时器相关的库函数主要集中在固件库文件 stm32f10x_tim.h 和 stm32f10x_tim.c 文件 中。 1)TIM3 时钟使能。 TIM3 是挂载在 APB1 之下,所以我们通过 APB1 总线下的使能使能函数来使能 TIM3。调 用的函数是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //时钟使能 2)初始化定时器参数,设置自动重装值,分频系数,计数方式等。 在库函数中,定时器的初始化参数是通过初始化函数 TIM_TimeBaseInit 实现的: voidTIM_TimeBaseInit(TIM_TypeDef*TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct); 第一个参数是确定是哪个定时器,这个比较容易理解。第二个参数是定时器初始化参数结 构体指针,结构体类型为 TIM_TimeBaseInitTypeDef,下面我们看看这个结构体的定义: typedef struct { uint16_t TIM_Prescaler; uint16_t TIM_CounterMode; uint16_t TIM_Period; uint16_t TIM_ClockDivision; uint8_t TIM_RepetitionCounter; } TIM_TimeBaseInitTypeDef; 这个结构体一共有 5 个成员变量,要说明的是,对于通用定时器只有前面四个参数有用, 最后一个参数 TIM_RepetitionCounter 是高级定时器才有用的,这里不多解释。 第一个参数 TIM_Prescaler 是用来设置分频系数的,刚才上面有讲解。 187 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二个参数 TIM_CounterMode 是用来设置计数方式,上面讲解过,可以设置为向上计数, 向下计数方式还有中央对齐计数方式,比较常用的是向上计数模式 TIM_CounterMode_Up 和向 下计数模式 TIM_CounterMode_Down。 第三个参数是设置自动重载计数周期值,这在前面也已经讲解过。 第四个参数是用来设置时钟分频因子。 针对 TIM3 初始化范例代码格式: TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = 5000; TIM_TimeBaseStructure.TIM_Prescaler =7199; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); 3)设置 TIM3_DIER 允许更新中断。 因为我们要使用 TIM3 的更新中断,寄存器的相应位便可使能更新中断。在库函数里面定 时器中断使能是通过 TIM_ITConfig 函数来实现的: void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState); 第一个参数是选择定时器号,这个容易理解,取值为 TIM1~TIM17。 第二个参数非常关键,是用来指明我们使能的定时器中断的类型,定时器中断的类型有很 多种,包括更新中断 TIM_IT_Update,触发中断 TIM_IT_Trigger,以及输入捕获中断等等。 第三个参数就很简单了,就是失能还是使能。 例如我们要使能 TIM3 的更新中断,格式为: TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); 4)TIM3 中断优先级设置。 在定时器中断使能之后,因为要产生中断,必不可少的要设置 NVIC 相关寄存器,设置中 断优先级。之前多次讲解到用 NVIC_Init 函数实现中断优先级的设置,这里就不重复讲解。 5)允许 TIM3 工作,也就是使能 TIM3。 光配置好定时器还不行,没有开启定时器,照样不能用。我们在配置完后要开启定时器, 通过 TIM3_CR1 的 CEN 位来设置。在固件库里面使能定时器的函数是通过 TIM_Cmd 函数来实 现的: void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState) 这个函数非常简单,比如我们要使能定时器 3,方法为: TIM_Cmd(TIM3, ENABLE); //使能 TIMx 外设 6)编写中断服务函数。 在最后,还是要编写定时器中断服务函数,通过该函数来处理定时器产生的相关中断。在 中断产生后,通过状态寄存器的值来判断此次产生的中断属于什么类型。然后执行相关的操作, 我们这里使用的是更新(溢出)中断,所以在状态寄存器 SR 的最低位。在处理完中断之后应 该向 TIM3_SR 的最低位写 0,来清除该中断标志。 在固件库函数里面,用来读取中断状态寄存器的值判断中断类型的函数是: ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t) 该函数的作用是,判断定时器 TIMx 的中断类型 TIM_IT 是否发生中断。比如,我们要判断定 时器 3 是否发生更新(溢出)中断,方法为: if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){} 固件库中清除中断标志位的函数是: 188 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT) 该函数的作用是,清除定时器 TIMx 的中断 TIM_IT 标志位。使用起来非常简单,比如我们在 TIM3 的溢出中断发生后,我们要清除中断标志位,方法是: TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); 这里需要说明一下,固件库还提供了两个函数用来判断定时器状态以及清除定时器状态标 志位的函数 TIM_GetFlagStatus 和 TIM_ClearFlag,他们的作用和前面两个函数的作用类似。只 是在 TIM_GetITStatus 函数中会先判断这种中断是否使能,使能了才去判断中断标志位,而 TIM_GetFlagStatus 直接用来判断状态标志位。 通过以上几个步骤,我们就可以达到我们的目的了,使用通用定时器的的更新中断,来控 制 DS1 的亮灭。 12.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 定时器 TIM3 本章将通过 TIM3 的中断来控制 DS1 的亮灭,DS0 和 DS1 的电路在前面已经有介绍了。而 TIM3 属于 STM32 的内部资源,只需要软件设置即可正常工作。 12.3 软件设计 软件设计我们直接打开我们光盘实验 7 定时器中断实验即可。我们可以看到我们的工程中 的 HARDWARE 下面比以前多了一个 time.c 文件(包括头文件 time.h),这两个文件是我们自己 编写。同时还引入了定时器相关的固件库函数文件 stm32f10x_tim.c 和头文件 stm32f10x_tim.h。 下面我们来看看我们的 time.c 文件。timer.c 文件内容如下 #include "timer.h" #include "led.h" //通用定时器 3 中断初始化 //这里时钟选择为 APB1 的 2 倍,而 APB1 为 36M //arr:自动重装值。 //psc:时钟预分频数 //这里使用的是定时器 3! void TIM3_Int_Init(u16 arr,u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //①时钟 TIM3 使能 //定时器 TIM3 初始化 TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载寄存器周期的值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置时钟频率除数的预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数 TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //②初始化 TIM3 189 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE ); //③允许更新中断 //中断优先级 NVIC 设置 NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; //TIM3 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //先占优先级 0 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级 3 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能 NVIC_Init(&NVIC_InitStructure); //④初始化 NVIC 寄存器 TIM_Cmd(TIM3, ENABLE); //⑤使能 TIM3 } //定时器 3 中断服务程序⑥ void TIM3_IRQHandler(void) //TIM3 中断 { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) //检查 TIM3 更新中断发生与否 { TIM_ClearITPendingBit(TIM3, TIM_IT_Update ); //清除 TIM3 更新中断标志 LED1=!LED1; } } 该文件下包含一个中断服务函数和一个定时器 3 中断初始化函数,中断服务函数比较简单, 在每次中断后,判断 TIM3 的中断类型,如果中断类型正确(溢出中断),则执行 LED1(DS1) 的取反。 TIM3_Int_Init()函数就是执行我们上面 12.1 节介绍的那 6 个步骤,我们分别用标号①~⑥来 标注,该函数的 2 个参数用来设置 TIM3 的溢出时间。在前面时钟系统部分我们讲解过,系统 初始化的时候在默认的系统初始化函数 SystemInit 函数里面已经初始化 APB1 的时钟为 2 分频, 所以 APB1 的时钟为 36M,而从 STM32 的内部时钟树图得知:当 APB1 的时钟分频数为 1 的 时候,TIM2~7 的时钟为 APB1 的时钟,而如果 APB1 的时钟分频数不为 1,那么 TIM2~7 的时 钟频率将为 APB1 时钟的两倍。因此,TIM3 的时钟为 72M,再根据我们设计的 arr 和 psc 的值, 就可以计算中断时间了。计算公式如下: 其中: Tout= ((arr+1)*(psc+1))/Tclk; Tclk:TIM3 的输入时钟频率(单位为 Mhz)。 Tout:TIM3 溢出时间(单位为 us)。 timer.h 文件的代码非常简单,一些函数申明,这里就不讲解。 最后,我们在主程序里面输入如下代码: int main(void) { delay_init(); //延时函数初始化 NVIC_Configuration(); //设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 uart_init(9600); //串口初始化波特率为 9600 190 STM32 不完全手册(库函数版) LED_Init(); TIM3_Int_Init(4999,7199); ALIENTEK MiniSTM32 V3.0 开发板教程 //LED 端口初始化 //10Khz 的计数频率,计数到 5000 为 500ms while(1) { LED0=!LED0; delay_ms(200); } } 这里的代码和之前大同小异,此段代码对 TIM3 进行初始化之后,进入死循环等待 TIM3 溢出中断,当 TIM3_CNT 的值等于 TIM3_ARR 的值的时候,就会产生 TIM3 的更新中断, 然后在中断里面取反 LED1,TIM3_CNT 再从 0 开始计数。 12.4 下载验证 在完成软件设计之后,我们将编译好的文件下载到 MiniSTM32 开发板上,观看其运行结果 是否与我们编写的一致。如果没有错误,我们将看 DS0 不停闪烁(每 400ms 闪烁一次),而 DS1 也是不停的闪烁,但是闪烁时间较 DS0 慢(1s 一次)。 191 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十三章 PWM 输出实验 上一章,我们介绍了 STM32 的通用定时器 TIM3,用该定时器的中断来控制 DS1 的闪烁, 这一章,我们将向大家介绍如何使用 STM32 的定时器来产生 PWM 输出。在本章中,我们将使 用 TIM1 的通道 1 产生 PWM 来控制 DS0 的亮度。本章分为如下几个部分: 13.1 PWM 简介 13.2 硬件设计 13.3 软件设计 13.4 下载验证 192 13.1 PWM 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用 微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽 度的控制。 STM32 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。其中高级定 时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4 路的 PWM 输出,这样,STM32 最多可以同时产生 30 路 PWM 输出!这里我们仅使用 TIM1 的 CH1 产生一路 PWM 输出。如果要产生多路输出,大家可以根据我们的代码稍作修改即可。 要使 STM32 的高级定时器 TIM1 产生 PWM 输出,除了上一章介绍的几个寄存器(ARR、 PSC、CR1 等)外,我们还会用到 4 个寄存器(通用定时器则只需要 3 个),来控制 PWM 的输 出。这四个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、捕获/比较使能寄存器 (TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)以及刹车和死区寄存器(TIMx_BDTR)。 接下来我们简单介绍一下这四个寄存器。 首先是捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器总共有 2 个,TIMx _CCMR1 和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。该寄存器 的各位描述如图 13.1.1 所示: 图 13.1.1 TIMx_CCMR1 寄存器各位描述 该寄存器的有些位在不同模式下,功能不一样,所以在图 13.1.1 中,我们把寄存器分了 2 层,上面一层对应输出时的设置而下面的则对应输入时的设置。关于该寄存器的详细说明,请 参考《STM32 参考手册》第 240 页,13.4.7 一节。这里我们需要说明的是模式设置位 OCxM, 此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,这 3 位必须设置为 110/111。这两种 PWM 模式的区别就是输出电平的极性相反。另外 CCxS 用于设置通道的方向 (输入/输出)默认设置为 0,就是设置通道作为输出使用。 接下来,我们介绍捕获/比较使能寄存器(TIMx_CCER),该寄存器控制着各个输入输出通 道的开关。该寄存器的各位描述如图 13.1.2 所示: 图 13.1.2 TIMx_ CCER 寄存器各位描述 该寄存器比较简单,我们这里只用到了 CC1E 位,该位是输入/捕获 1 输出使能位,要想 PWM 从 IO 口输出,这个位必须设置为 1,所以我们需要设置该位为 1。该寄存器更详细的介 绍了,请参考《STM32 参考手册》第 244 页,13.4.9 这一节。 最后,我们介绍一下捕获/比较寄存器(TIMx_CCR1~4),该寄存器总共有 4 个,对应 4 个 输通道 CH1~4。因为这 4 个寄存器都差不多,我们仅以 TIMx_CCR1 为例介绍,该寄存器的各 位描述如图 13.1.3 所示: 193 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 13.1.3 寄存器 TIMx_ CCR1 各位描述 在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点, 我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。本章,我们使用的是 TIM1 的通道 1,所以我们需要修改 TIM1_CCR1 以实现脉宽控制 DS0 的亮度。 如果是通用定时器,则配置以上三个寄存器就够了,但是如果是高级定时器,则还需要配 置:刹车和死区寄存器(TIMx_BDTR),该寄存器各位描述如图 13.1.4 所示: 图 13.1.4 寄存器 TIMx_ BDTR 各位描述 该寄存器,我们只需要关注最高位:MOE 位,要想高级定时器的 PWM 正常输出,则必须 设置 MOE 位为 1,否则不会有输出。注意:通用定时器不需要配置这个。其他位我们这里就不 详细介绍了,请参考《STM32 参考手册》第 248 页,13.4.18 这一节。 至此,我们把本章要用的几个相关寄存器都介绍完了,本章要实现通过 TIM1_CH1 输出 PWM 来控制 DS0 的亮度。下面我们介绍配置步骤: 1)开启 TIM1 时钟,配置 PA8 为复用输出。 要使用 TIM1,我们必须先开启 TIM1 的时钟,这点相信大家看了这么多代码,应该明白了。 这里我们还要配置 PA8 为复用输出(当然还要时能 PORTA 的时钟),这是因为 TIM1_CH1 通 道将使用 PA8 的复用功能作为输出。库函数使能 TIM3 时钟的方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定时器 3 时钟 设置 PA8 为复用功能输出的方法在前面的几个实验都有类似的讲解,相信大家很明白,这里简单 列出 GPIO 初始化的一行代码即可: GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 2)设置 TIM1 的 ARR 和 PSC。 在开启了 TIM1 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来控制输出 PWM 的 周期。当 PWM 周期太慢(低于 50Hz)的时候,我们就会明显感觉到闪烁了。因此,PWM 周 期在这里不宜设置的太小。这在库函数是通过 TIM_TimeBaseInit 函数实现的,在上一节定时器 中断章节我们已经有讲解,这里就不详细讲解,调用的格式为: 194 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式 TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx 的 3)设置 TIM1_CH1 的 PWM 模式及通道方向, 使能 TIM1 的 CH1 输出。 接下来,我们要设置 TIM1_CH1 为 PWM 模式(默认是冻结的),因为我们的 DS0 是低电 平亮,而我们希望当 CCR1 的值小的时候,DS0 就暗,CCR1 值大的时候,DS0 就亮,所以我 们要通过配置 TIM1_CCMR1 的相关位来控制 TIM1_CH1 的模式。在库函数中,PWM 通道设 置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的,不同的通道的设置函数不一样,这里我 们使用的是通道 1,所以使用的函数是 TIM_OC1Init()。 void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); 这种初始化格式大家学到这里应该也熟悉了,所以我们直接来看看结构体 TIM_OCInitTypeDef 的定义: typedef struct { uint16_t TIM_OCMode; uint16_t TIM_OutputState; uint16_t TIM_OutputNState; */ uint16_t TIM_Pulse; uint16_t TIM_OCPolarity; uint16_t TIM_OCNPolarity; uint16_t TIM_OCIdleState; uint16_t TIM_OCNIdleState; } TIM_OCInitTypeDef; 这里我们讲解一下与我们要求相关的几个成员变量: 参数 TIM_OCMode 设置模式是 PWM 还是输出比较,这里我们是 PWM 模式。 参数 TIM_OutputState 用来设置比较输出使能,也就是使能 PWM 输出到端口。 参数 TIM_OCPolarity 用来设置极性是高还是低。 其他的参数 TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState 是 高级定时器 TIM1 和 TIM8 才用到的。 要实现我们上面提到的场景,方法是: TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择 PWM 模式 2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性高 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //初始化 TIM1 OC1 4)使能 TIM1。 在完成以上设置了之后,我们需要使能 TIM1。使能 TIM1 的方法前面已经讲解过: TIM_Cmd(TIM1, ENABLE); //使能 TIM1 5)设置 MOE 输出,使能 PWM 输出。 普通定时器在完成以上设置了之后,就可以输出 PWM 了,但是高级定时器,我们还需要 使能刹车和死区寄存器(TIM1_BDTR)的 MOE 位,以使能整个 OCx(即 PWM)输出。库函 195 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 数的设置函数为: TIM_CtrlPWMOutputs(TIM1,ENABLE);// MOE 主输出使能 6)修改 TIM1_CCR1 来控制占空比。 最后,在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定 的,而我们通过修改 TIM1_CCR1 则可以控制 CH1 的输出占空比。继而控制 DS0 的亮度。 在库函数中,修改 TIM1_CCR1 占空比的函数是: void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1); 理所当然,对于其他通道,分别有一个函数名字,函数格式为 TIM_SetComparex(x=1,2,3,4)。 通过以上 6 个步骤,我们就可以控制 TIM1 的 CH1 输出 PWM 波了。 13.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 定时器 TIM3 这两个前面都有介绍,但是我们这里用到了 TIM1_CH1 通道的输出,从原理图(图 6.2.1) 可以看到,TIM1_CH1 是和 PA8 相连的,所以电路上并没有任何变化。 13.3 软件设计 打开光盘里面的 PWM 输出实验代码,可以看见我们在工程中添加了 pwm.c 文件,并且引 入了头文件 pwm.h。打开 pwm.c 内容如下: #include "pwm.h" #include "led.h" //PWM 输出初始化 //arr:自动重装值 //psc:时钟预分频数 void TIM1_PWM_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);// ①使能 tim1 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA , ENABLE); //①使能 GPIO 外设时钟使能 //设置该引脚为复用输出功能,输出 TIM1 CH1 的 PWM 脉冲波形 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //TIM_CH1 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值 80K 196 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置用来作为 TIMx 时钟频率除数的预分频值 不分频 TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数 TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); //②初始化 TIMx TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //脉宽调制模式 2 TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能 TIM_OCInitStructure.TIM_Pulse = 0; //设置待装入捕获比较寄存器的脉冲值 TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性高 TIM_OC1Init(TIM1, &TIM_OCInitStructure); //③初始化外设 TIMx TIM_CtrlPWMOutputs(TIM1,ENABLE); //⑤MOE 主输出使能 TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); //CH1 预装载使能 TIM_ARRPreloadConfig(TIM1, ENABLE); //使能 TIMx 在 ARR 上的预装载寄存器 TIM_Cmd(TIM1, ENABLE); //④使能 TIM1 } 此部分代码包含了上面介绍的 PWM 输出设置的前 5 个步骤。这里我们关于 TIM1 的设置 就不再说了。 头文件 pwm.h 主要是函数申明,这里就不做过多讲解。 接下来,我们看看主函数 main 内容如下: int main(void) { u16 led0pwmval=0; u8 dir=1; delay_init(); //延时函数初始化 LED_Init(); //初始化与 LED 连接的硬件接口 TIM1_PWM_Init(899,0);//不分频。PWM 频率=72000/(899+1)=80Khz while(1) { delay_ms(10); if(dir)led0pwmval++; else led0pwmval--; if(led0pwmval>300)dir=0; if(led0pwmval==0)dir=1; TIM_SetCompare1(TIM1,led0pwmval); } } 这里,我们从死循环函数可以看出,我们控制 LED0_PWM_VAL 的值从 0 变到 300,然后 又从 300 变到 0,如此循环,因此 DS0 的亮度也会跟着从暗变到亮,然后又从亮变到暗。至于 197 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 这里的值,我们为什么取 300,是因为 PWM 的输出占空比达到这个值的时候,我们的 LED 亮 度变化就不大了(虽然最大值可以设置到 899),因此设计过大的值在这里是没必要的。至此, 我们的软件设计就完成了。 13.4 下载验证 在完成软件设计之后,将我们将编译好的文件下载到 MiniSTM32 开发板上,观看其运行结 果是否与我们编写的一致。如果没有错误,我们将看 DS0 不停的由暗变到亮,然后又从亮变到 暗。每个过程持续时间大概为 3 秒钟左右。 实际运行结果如下图 13.4.1 所示: 图 13.4.1 PWM 控制 DS0 亮度 198 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十四章 输入捕获实验 上一章,我们介绍了 STM32 的定时器作为 PWM 输出的使用方法,这一章,我们将向大家 介绍通用定时器作为输入捕获的使用。在本章中,我们将用 TIM2 的通道 1(PA0)来做输入捕 获,捕获 PA0 上高电平的脉宽(用 WK_UP 按键输入高电平),通过串口打印高电平脉宽时间, 从本章分为如下几个部分: 14.1 输入捕获简介 14.2 硬件设计 14.3 软件设计 14.4 下载验证 199 14.1 输入捕获简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 输入捕获模式可以用来测量脉冲宽度或者测量频率。STM32 的定时器,除了 TIM6 和 TIM7, 其他定时器都有输入捕获功能。STM32 的输入捕获,简单的说就是通过检测 TIMx_CHx 上的 边沿信号,在边沿信号发生跳变(比如上升沿/下降沿)的时候,将当前定时器的值(TIMx_CNT) 存放到对应的通道的捕获/比较寄存器(TIMx_CCRx)里面,完成一次捕获。同时还可以配置 捕获时是否触发中断/DMA 等。 本章我们用到 TIM2_CH1 来捕获高电平脉宽,也就是要先设置输入捕获为上升沿检测,记 录发生上升沿的时候 TIM2_CNT 的值。然后配置捕获信号为下降沿捕获,当下降沿到来时,发 生捕获,并记录此时的 TIM2_CNT 值。这样,前后两次 TIM2_CNT 之差,就是高电平的脉宽, 同时 TIM2 的计数频率我们是知道的,从而可以计算出高电平脉宽的准确时间。 接下来,我们介绍我们本章需要用到的一些寄存器配置,需要用到的寄存器有:TIMx_ARR、 TIMx_PSC、TIMx_CCMR1、TIMx_CCER、TIMx_DIER、TIMx_CR1、TIMx_CCR1 这些寄存 器在前面两章全部都有提到(这里的 x=2),我们这里就不再全部罗列了,我们这里针对性的介绍 这几个寄存器的配置。 首先 TIMx_ARR 和 TIMx_PSC,这两个寄存器用来设自动重装载值和 TIMx 的时钟分频, 用法同前面介绍的,我们这里不再介绍。 再来看看捕获/比较模式寄存器 1:TIMx_CCMR1,这个寄存器在输入捕获的时候,非常有 用,有必要重新介绍,该寄存器的各位描述如图 14.1.1 所示: 图 14.1.1 TIMx_CCMR1 寄存器各位描述 当在输入捕获模式下使用的时候,对应图 14.1.1 的第二行描述,从图中可以看出, TIMx_CCMR1 明显是针对 2 个通道的配置,低八位[7:0]用于捕获/比较通道 1 的控制,而高八 位[15:8]则用于捕获/比较通道 2 的控制,因为 TIMx 还有 CCMR2 这个寄存器,所以可以知道 CCMR2 是用来控制通道 3 和通道 4(详见《STM32 参考手册》290 页,14.4.8 节)。 这里我们用到的是 TIM2 的捕获/比较通道 1,我们重点介绍 TIMx_CMMR1 的[7:0]位(其 实高 8 位配置类似),TIMx_CMMR1 的[7:0]位详细描述见图 14.1.2 所示: 200 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 14.1.2 TIMx_CMMR1 [7:0]位详细描述 其中 CC1S[1:0],这两个位用于 CCR1 的通道方向配置,这里我们设置 IC1S[1:0]=01,也就 是配置为输入,且 IC1 映射在 TI1 上(关于 IC1,TI1 不明白的,可以看《STM32 参考手册》 14.2 节的图 98-通用定时器框图),CC1 即对应 TIMx_CH1。 输入捕获 1 预分频器 IC1PSC[1:0],这个比较好理解。我们是 1 次边沿就触发 1 次捕获,所 以选择 00 就是了。 输入捕获 1 滤波器 IC1F[3:0],这个用来设置输入采样频率和数字滤波器长度。其中, 是定时器的输入频率(TIMxCLK),一般为 72Mhz,而 则是根据 TIMx_CR1 的 CKD[1:0] 的设置来确定的,如果 CKD[1:0]设置为 00,那么 = 。N 值就是滤波长度,举个简 单的例子:假设 IC1F[3:0]=0011,并设置 IC1 映射到通道 1 上,且为上升沿触发,那么在捕获 到上升沿的时候,再以 的频率,连续采样到 8 次通道 1 的电平,如果都是高电平,则说 明确实是一个有效的触发,就会触发输入捕获中断(如果开启了的话)。这样可以滤除那些高电 平脉宽低于 8 个采样周期的脉冲信号,从而达到滤波的效果。这里,我们不做滤波处理,所以 设置 IC1F[3:0]=0000,只要采集到上升沿,就触发捕获。 201 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 再来看看捕获/比较使能寄存器:TIMx_CCER,该寄存器的各位描述见图 13.1.2(在第 13 章)。本章我们要用到这个寄存器的最低 2 位,CC1E 和 CC1P 位。这两个位的描述如图 14.1.3 所示: 图 14.1.3 TIMx_CCER 最低 2 位描述 所以,要使能输入捕获,必须设置 CC1E=1,而 CC1P 则根据自己的需要来配置。 接下来我们再看看 DMA/中断使能寄存器:TIMx_DIER,该寄存器的各位描述见图 12.1.2 (在第 12 章),本章,我们需要用到中断来处理捕获数据,所以必须开启通道 1 的捕获比较中 断,即 CC1IE 设置为 1。 控制寄存器:TIMx_CR1,我们只用到了它的最低位,也就是用来使能定时器的,这里前 面两章都有介绍,请大家参考前面的章节。 最后再来看看捕获/比较寄存器 1:TIMx_CCR1,该寄存器用来存储捕获发生时,TIMx_CNT 的值,我们从 TIMx_CCR1 就可以读出通道 1 捕获发生时刻的 TIMx_CNT 值,通过两次捕获(一 次上升沿捕获,一次下降沿捕获)的差值,就可以计算出高电平脉冲的宽度。 至此,我们把本章要用的几个相关寄存器都介绍完了,本章要实现通过输入捕获,来获取 TIM2_CH1(PA0)上面的高电平脉冲宽度,并从串口打印捕获结果。下面我们介绍库函数设置输 入捕获的配置步骤: 1)开启 TIM2 时钟,配置 PA0 为下拉输入。 要使用 TIM2,我们必须先开启 TIM2 的时钟。这里我们还要配置 PA0 为下拉输入,因为 我们要捕获 TIM2_CH1 上面的高电平脉宽,而 TIM2_CH1 是连接在 PA0 上面的。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能 TIM2 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能 GPIOA 时钟 这两个函数的使用在前面多次提到,还有 GPIO 初始化,这里也不重复了。 2)初始化 TIM2,设置 TIM2 的 ARR 和 PSC。 在开启了 TIM2 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来设置输入捕获的 自动重装载值和计数频率。这在库函数中是通过 TIM_TimeBaseInit 函数实现的,在上面章节已 经讲解过,这里不重复讲解。 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值 202 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM 向上计数模式 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //根据指定的参数初始化 Tim2 3)设置 TIM2 的输入比较参数,开启输入捕获 输入比较参数的设置包括映射关系,滤波,分频以及捕获方式等。这里我们需要设置通道 1 为输入模式,且 IC1 映射到 TI1(通道 1)上面,并且不使用滤波(提高响应速度)器,上升沿捕 获。库函数是通过 TIM_ICInit 函数来初始化输入比较参数的: void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct); 同样,我们来看看参数设置结构体 TIM_ICInitTypeDef 的定义: typedef struct { uint16_t TIM_Channel; uint16_t TIM_ICPolarity; uint16_t TIM_ICSelection; uint16_t TIM_ICPrescaler; uint16_t TIM_ICFilter; } TIM_ICInitTypeDef; 参数 TIM_Channel 很好理解,用来设置通道。我们设置为通道 1,为 TIM_Channel_1。 参 数 TIM_ICPolarit 是 用 来 设 置 输 入 信 号 的 有 效 捕 获 极 性 , 这 里 我 们 设 置 为 TIM_ICPolarity_Rising,上升沿捕获。同时库函数还提供了单独设置通道 1 捕获极性的函数为: TIM_OC1PolarityConfig(TIM2,TIM_ICPolarity_Falling), 这表示通道 1 为上升沿捕获,我们后面会用到,同时对于其他三个通道也有一个类似的函数, 使用的时候一定要分清楚使用的是哪个通道该调用哪个函数,格式为 TIM_OCxPolarityConfig()。 参 数 TIM_ICSelection 是 用 来 设 置 映 射 关 系 , 我 们 配 置 IC1 直 接 映 射 在 TI1 上 , 选 择 TIM_ICSelection_DirectTI。 参 数 TIM_ICPrescaler 用 来 设 置 输 入 捕 获 分 频 系 数 , 我 们 这 里 不 分 频 , 所 以 选 中 TIM_ICPSC_DIV1,还有 2,4,8 分频可选。 参数 TIM_ICFilter 设置滤波器长度,这里我们不使用滤波器,所以设置为 0。 这些参数的意义,在我们讲解寄存器的时候举例说明过,这里不做详细解释。 我们的配置代码是: TIM_ICInitTypeDef TIM2_ICInitStructure; TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择输入端 IC1 映射到 TI1 上 TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到 TI1 上 TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波 TIM_ICInit(TIM2, &TIM2_ICInitStructure); 4)使能捕获和更新中断(设置 TIM2 的 DIER 寄存器) 因为我们要捕获的是高电平信号的脉宽,所以,第一次捕获是上升沿,第二次捕获时下降 沿,必须在捕获上升沿之后,设置捕获边沿为下降沿,同时,如果脉宽比较长,那么定时器就 203 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 会溢出,对溢出必须做处理,否则结果就不准了。这两件事,我们都在中断里面做,所以必须 开启捕获中断和更新中断。 这里我们使用定时器的开中断函数 TIM_ITConfig 即可使能捕获和更新中断: TIM_ITConfig( TIM2,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断和捕获中断 5)设置中断分组,编写中断服务函数 设置中断分组的方法前面多次提到这里我们不做讲解,主要是通过函数 NVIC_Init()来完成。 分组完成后,我们还需要在中断函数里面完成数据处理和捕获设置等关键操作,从而实现高电 平脉宽统计。在中断服务函数里面,跟以前的外部中断和定时器中断实验中一样,我们在中断 开始的时候要进行中断类型判断,在中断结束的时候要清除中断标志位。使用到的函数在上面 的实验已经讲解过,分别为 TIM_GetITStatus()函数和 TIM_ClearITPendingBit()函数。 if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET){}//判断是否为更新中断 if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET){}//判断是否发生捕获事件 TIM_ClearITPendingBit(TIM2, TIM_IT_CC1|TIM_IT_Update);//清除中断和捕获标志位 6)使能定时器(设置 TIM2 的 CR1 寄存器) 最后,必须打开定时器的计数器开关, 启动 TIM5 的计数器,开始输入捕获。 TIM_Cmd(TIM2,ENABLE ); //使能定时器 2 通过以上 6 步设置,定时器 2 的通道 1 就可以开始输入捕获了,同时因为还用到了串口输 出结果,所以还需要配置一下串口。 14.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 按键 3) 串口 4) 定时器 TIM3 5) 定时器 TIM2 前面 4 个,在之前的章节均有介绍。本节,我们将捕获 TIM2_CH1(PA0)上的高电平脉 宽,通过 WK_UP 按键输入高电平,并从串口打印高电平脉宽。同时我们保留上节的 PWM 输 出,大家也可以通过用杜邦线连接 PA8 和 PA0,来测量 PWM 输出的高电平脉宽。 14.3 软件设计 打开光盘的输入捕获实验,可以看到,我们的输入捕获代码是直接添加在 timer.c 和 timer.h 中。同时输入捕获相关的库函数还是在 stm32f10x_tim.c 和 stm32f10x_tim.h 文件中。 我们在 timer.c 里面加入如下代码: //定时器 2 通道 1 输入捕获配置 TIM_ICInitTypeDef TIM2_ICInitStructure; void TIM2_Cap_Init(u16 arr,u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //使能 TIM2 时钟 204 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能 GPIOA 时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0 清除之前设置 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 输入 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_ResetBits(GPIOA,GPIO_Pin_0); //PA0 下拉 //初始化定时器 2 TIM2 TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值 TIM_TimeBaseStructure.TIM_Prescaler =psc; //预分频器 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割 TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); //初始化 TIMx 的时间基数单位 //初始化 TIM2 输入捕获参数 TIM2_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择输入端 IC1 映射到 TI1 上 TIM2_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM2_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到 TI1 上 TIM2_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM2_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波 TIM_ICInit(TIM2, &TIM2_ICInitStructure); //中断分组初始化 NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; //TIM2 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级 0 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能 NVIC_Init(&NVIC_InitStructure); //初始化外设 NVIC 寄存器 TIM_ITConfig(TIM2,TIM_IT_Update|TIM_IT_CC1,ENABLE); //允许更新中断 CC1IE 捕获中断 TIM_Cmd(TIM2,ENABLE ); //使能定时器 2 } u8 TIM2CH1_CAPTURE_STA=0; //输入捕获状态 u16 TIM2CH1_CAPTURE_VAL;//输入捕获值 //定时器 5 中断服务程序 void TIM2_IRQHandler(void) { if((TIM2CH1_CAPTURE_STA&0X80)==0)//还未成功捕获 { 205 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { if(TIM2CH1_CAPTURE_STA&0X40)//已经捕获到高电平了 { if((TIM2CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了 { TIM2CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次 TIM2CH1_CAPTURE_VAL=0XFFFF; }else TIM2CH1_CAPTURE_STA++; } } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)//捕获 1 发生捕获事件 { if(TIM2CH1_CAPTURE_STA&0X40) //捕获到一个下降沿 { TIM2CH1_CAPTURE_STA|=0X80; //标记成功捕获到一次上升沿 TIM2CH1_CAPTURE_VAL=TIM_GetCapture1(TIM2); TIM_OC1PolarityConfig(TIM2,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获 }else //还未开始,第一次捕获上升沿 { TIM2CH1_CAPTURE_STA=0; //清空 TIM2CH1_CAPTURE_VAL=0; TIM_SetCounter(TIM2,0); TIM2CH1_CAPTURE_STA|=0X40; //标记捕获到了上升沿 TIM_OC1PolarityConfig(TIM2,TIM_ICPolarity_Falling); //CC1P=1 设置为下降沿捕获 } } } TIM_ClearITPendingBit(TIM2, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位 } 此部分代码包含 2 个函数,其中 TIM2_Cap_Init 函数用于 TIM2 通道 1 的输入捕获设置, 其设置和我们上面讲的步骤是一样的,这里就不多说,重点来看看第二个函数。 TIM2_IRQHandler 是 TIM2 的中断服务函数,该函数用到了两个全局变量,用于辅助实现 高电平捕获。其中 TIM2CH1_CAPTURE_STA,是用来记录捕获状态,该变量类似我们在 usart.c 里面自行定义的 USART_RX_STA 寄存器(其实就是个变量,只是我们把它当成一个寄存器那样 来使用)。TIM2CH1_CAPTURE_STA 各位描述如表 14.3.1 所示: TIM2CH1_CAPTURE_STA bit7 bit6 捕获完成标志 捕获到高电平标志 bit5~0 捕获高电平后定时器溢出的次数 206 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 表 14.3.1 TIM2CH1_CAPTURE_STA 各位描述 另外一个变量 TIM2CH1_CAPTURE_VAL,则用来记录捕获到下降沿的时候,TIM2_CNT 的值。 现在我们来介绍一下,捕获高电平脉宽的思路:首先,设置 TIM2_CH1 捕获上升沿,这在 TIM2_Cap_Init 函数执行的时候就设置好了,然后等待上升沿中捕获断到来,当捕获到上升沿 中断,此时如果 TIM2CH1_CAPTURE_STA 的第 6 位为 0,则表示还没有捕获到新的上升沿, 就先把 TIM2CH1_CAPTURE_STA、TIM2CH1_CAPTURE_VAL 和 TIM2->CNT 等清零,然后 再设置 TIM2CH1_CAPTURE_STA 的第 6 位为 1,标记捕获到高电平,最后设置为下降沿捕获, 等待下降沿到来。如果等待下降沿到来期间,定时器发生了溢出,就在 TIM2CH1_CAPTURE_STA 里面对溢出次数进行计数,当最大溢出次数来到的时候,就强制标 记 捕 获 完 成 ( 虽 然 此 时 还 没 有 捕 获 到 下 降 沿 )。 当 下 降 沿 到 来 的 时 候 , 先 设 置 TIM2CH1_CAPTURE_STA 的第 7 位为 1,标记成功捕获一次高电平,然后读取此时的定时器 的捕获值到 TIM2CH1_CAPTURE_VAL 里面,最后设置为上升沿捕获,回到初始状态。 这样,我们就完成一次高电平捕获了,只要 TIM2CH1_CAPTURE_STA 的第 7 位一直为 1, 那么就不会进行第二次捕获,我们在 main 函数处理完捕获数据后,将 TIM2CH1_CAPTURE_STA 置零,就可以开启第二次捕获。 这里我们还使用到一个函数 TIM_OC1PolarityConfig 来修改输入捕获通道 1 的极性的。相信这 个不难理解: void TIM_OC1PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity) 要设置为上升沿捕获,则为: TIM_OC1PolarityConfig(TIM2,TIM_ICPolarity_Rising); //设置为上升沿捕获 还有一个函数用来设置计数器寄存器值,这个同样很好理解: TIM_SetCounter(TIM2,0); 上行代码的意思就是计数值清零。 接下来,我们修改主程序里面的 main 函数如下: #include "led.h" #include "delay.h" #include "sys.h" #include "timer.h" #include "usart.h" extern u8 TIM2CH1_CAPTURE_STA; //输入捕获状态 extern u16 TIM2CH1_CAPTURE_VAL;//输入捕获值 int main(void) { u32 temp=0; NVIC_Configuration();//设置 NVIC 中断分组 2:2 位抢占优先级,2 位响应优先级 delay_init(); uart_init(9600); //延时函数初始化 //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 TIM1_PWM_Init(899,0); //不分频。PWM 频率=72000/(899+1)=80Khz TIM2_Cap_Init(0XFFFF,72-1); //以 1Mhz 的频率计数 while(1) 207 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { delay_ms(10); TIM_SetCompare1(TIM1,TIM_GetCapture1(TIM1)+1); if(TIM_GetCapture1(TIM1)==300)TIM_SetCompare1(TIM1,0); if(TIM2CH1_CAPTURE_STA&0X80)//成功捕获到了一次高电平 { temp=TIM2CH1_CAPTURE_STA&0X3F; temp*=65536; //溢出时间总和 temp+=TIM2CH1_CAPTURE_VAL; //得到总的高电平时间 printf("HIGH:%d us\r\n",temp); //打印总的高点平时间 TIM2CH1_CAPTURE_STA=0; //开启下一次捕获 } } } 该 main 函数是在 PWM 实验的基础上修改来的,我们保留了 PWM 输出,同时通过设置 TIM2_Cap_Init(0XFFFF,72-1),将 TIM2_CH1 的捕获计数器设计为 1us 计数一次,并设置重装 载值为最大,所以我们的捕获时间精度为 1us。 主函数通过 TIM2CH1_CAPTURE_STA 的第 7 位,来判断有没有成功捕获到一次高电平, 如果成功捕获,则将高电平时间通过串口输出到电脑。 至此,我们的软件设计就完成了。 14.4 下载验证 在完成软件设计之后,将我们将编译好的文件下载到 MiniSTM32 开发板上,可以看到 DS0 的状态和上一章差不多,由暗亮的循环。说明程序已经正常在跑了,我们再打开串口调试助 手,选择对应的串口,然后按 WK_UP 按键,可以看到串口打印的高电平持续时间,如图 14.4.1 所示: 208 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 14.4.1 PWM 控制 DS0 亮度 从上图可以看出,我们正常按下按键的时间,一般是 200ms 以内。大家还可以用杜邦线连 接 PA0 和 PA8,看看上一节中我们设置的 PWM 输出的高电平是如何变化的。 209 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十五章 OLED 显示实验 前面几章的实例,均没涉及到液晶显示,这一章,我们将向大家介绍 OLED 的使用。在本 章中,我们将使用 MiniSTM32 开发板上的 OLED 模块接口,来点亮 OLED,并实现 ASCII 字 符的显示。本章分为如下几个部分: 15.1 OLED 简介 15.2 硬件设计 15.3 软件设计 15.4 下载验证 210 15.1 OLED 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 OLED,即有机发光二极管(Organic Light-Emitting Diode),又称为有机电激光显示(Organic Electroluminesence Display, OELD)。OLED 由于同时具备自发光,不需背光源、对比度高、 厚度薄、视角广、反应速度快、可用于挠曲性面板、使用温度范围广、构造及制程较简单等优 异之特性,被认为是下一代的平面显示器新兴应用技术。 LCD 都需要背光,而 OLED 不需要,因为它是自发光的。这样同样的显示,OLED 效果要 来得好一些。以目前的技术,OLED 的尺寸还难以大型化,但是分辨率确可以做到很高。在本 章中,我们使用的是 ALINETEK 的 OLED 显示模块,该模块有以下特点: 1)模块有单色和双色两种可选,单色为纯蓝色,而双色则为黄蓝双色。 2)尺寸小,显示尺寸为 0.96 寸,而模块的尺寸仅为 27mm*26mm 大小。 3)高分辨率,该模块的分辨率为 128*64。 4)多种接口方式,该模块提供了总共 4 种接口包括:6800、8080 两种并行接口方式、4 线 SPI 接口方式以及 IIC 接口方式(只需要 2 根线就可以控制 OLED 了!)。 5)不需要高压,直接接 3.3V 就可以工作了。 这里要提醒大家的是,该模块不和 5.0V 接口兼容,所以请大家在使用的时候一定要小心, 别直接接到 5V 的系统上去,否则可能烧坏模块。以上 4 种模式通过模块的 BS1 和 BS2 设置, BS1 和 BS2 的设置与模块接口模式的关系如表 15.1.1 所示: 接口方式 4 线 SPI IIC 8 位 6800 8 位 8080 BS1 0 1 0 1 BS2 0 0 1 1 表 15.1.1 OLED 模块接口方式设置表 表 15.1.1 中:“1”代表接 VCC,而“0”代表接 GND。 该模块的外观图如图 15.1.1 所示: 图 15.1.1 ALIENTEK OLED 模块外观图 ALIENTEK OLED 模块默认设置是:BS1 和 BS2 接 VCC ,即使用 8080 并口方式,如果 你想要设置为其他模式,则需要在 OLED 的背面,用烙铁修改 BS1 和 BS2 的设置。 模块的原理图如图 15.1.2 所示: 211 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 15.1.2 ALIENTEK OLED 模块原理图 该模块采用 8*2 的 2.54 排针与外部连接,总共有 16 个管脚,在 16 条线中,我们只用了 15 条,有一个是悬空的。15 条线中,电源和地线占了 2 条,还剩下 13 条信号线。在不同模式下, 我们需要的信号线数量是不同的,在 8080 模式下,需要全部 13 条,而在 IIC 模式下,仅需要 2 条线就够了!这其中有一条是共同的,那就是复位线 RST(RES),RST 上的低电平,将导致 OLED 复位,在每次初始化之前,都应该复位一下 OLED 模块。 ALIENTEK OLED 模块的控制器是 SSD1306,本章,我们将学习如何通过 STM32 来控制 该模块显示字符和数字,本章的实例代码将可以支持两种方式与 OLED 模块连接,一种是 8080 的并口方式,另外一种是 4 线 SPI 方式。 首先我们介绍一下模块的 8080 并行接口,8080 并行接口的发明者是 INTEL,该总线也被 广泛应用于各类液晶显示器,ALIENTEK OLED 模块也提供了这种接口,使得 MCU 可以快速 的访问 OLED。ALIENTEK OLED 模块的 8080 接口方式需要如下一些信号线: CS:OLED 片选信号。 WR:向 OLED 写入数据。 RD:从 OLED 读取数据。 D[7:0]:8 位双向数据线。 RST(RES):硬复位 OLED。 DC:命令/数据标志(0,读写命令;1,读写数据)。 模块的 8080 并口读/写的过程为:先根据要写入/读取的数据的类型,设置 DC 为高(数据) /低(命令),然后拉低片选,选中 SSD1306,接着我们根据是读数据,还是要写数据置 RD/WR 为低,然后: 在 RD 的上升沿, 使数据锁存到数据线(D[7:0])上; 212 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 在 WR 的上升沿,使数据写入到 SSD1306 里面; SSD1306 的 8080 并口写时序图如图 15.1.3 所示: 图 15.1.3 8080 并口写时序图 SSD1306 的 8080 并口读时序图如图 15.1.4 所示: 图 15.1.4 8080 并口读时序图 SSD1306 的 8080 接口方式下,控制脚的信号状态所对应的功能如表 15.1.2: 功能 RD WR CS DC 写命令 H ↑ L L 读状态 ↑ H L L 写数据 H ↑ L H 读数据 ↑ H L H 表 15.1.2 控制脚信号状态功能表 在 8080 方式下读数据操作的时候,我们有时候(例如读显存的时候)需要一个假读命 (Dummy Read),以使得微控制器的操作频率和显存的操作频率相匹配。在读取真正的数据之 前,由一个的假读的过程。这里的假读,其实就是第一个读到的字节丢弃不要,从第二个开始, 才是我们真正要读的数据。 一个典型的读显存的时序图,如图 15.1.5 所示: 213 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 15.1.5 读显存时序图 可以看到,在发送了列地址之后,开始读数据,第一个是 Dummy Read,也就是假读,我 们从第二个开始,才算是真正有效的数据。 并行接口模式就介绍到这里,我们接下来介绍一下 4 线串行(SPI)方式,4 先串口模式使 用的信号线有如下几条: CS:OLED 片选信号。 RST(RES):硬复位 OLED。 DC:命令/数据标志(0,读写命令;1,读写数据)。 SCLK:串行时钟线。在 4 线串行模式下,D0 信号线作为串行时钟线 SCLK。 SDIN:串行数据线。在 4 线串行模式下,D1 信号线作为串行数据线 SDIN。 模块的 D2 需要悬空,其他引脚可以接到 GND。在 4 线串行模式下,只能往模块写数据而 不能读数据。 在 4 线 SPI 模式下,每个数据长度均为 8 位,在 SCLK 的上升沿,数据从 SDIN 移入到 SSD1306,并且是高位在前的。DC 线还是用作命令/数据的标志线。在 4 线 SPI 模式下,写操 作的时序如图 15.1.6 所示: 图 15.1.6 4 线 SPI 写操作时序图 4 线串行模式就为大家介绍到这里。其他还有几种模式,在 SSD1306 的数据手册上都有详 细的介绍,如果要使用这些方式,请大家参考该手册。 接下来,我们介绍一下模块的显存,SSD1306 的显存总共为 128*64bit 大小,SSD1306 将 这些显存分为了 8 页,其对应关系如表 15.1.3 所示: 214 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 表 15.1.3 SSD1306 显存与屏幕对应关系表 可以看出,SSD1306 的每页包含了 128 个字节,总共 8 页,这样刚好是 128*64 的点阵大 小。因为每次写入都是按字节写入的,这就存在一个问题,如果我们使用只写方式操作模块, 那么,每次要写 8 个点,这样,我们在画点的时候,就必须把要设置的点所在的字节的每个位 都搞清楚当前的状态(0/1?),否则写入的数据就会覆盖掉之前的状态,结果就是有些不需要 显示的点,显示出来了,或者该显示的没有显示了。这个问题在能读的模式下,我们可以先读 出来要写入的那个字节,得到当前状况,在修改了要改写的位之后再写进 GRAM,这样就不会 影响到之前的状况了。但是这样需要能读 GRAM,对于 3 线或 4 线 SPI 模式,模块是不支持读 的,而且读->改->写的方式速度也比较慢。 所以我们采用的办法是在 STM32 的内部建立一个 OLED 的 GRAM(共 128*8 个字节),在 每次修改的时候,只是修改 STM32 上的 GRAM(实际上就是 SRAM),在修改完了之后,一次 性把 STM32 上的 GRAM 写入到 OLED 的 GRAM。当然这个方法也有坏处,就是对于那些 SRAM 很小的单片机(比如 51 系列)就比较麻烦了。 SSD1306 的命令比较多,这里我们仅介绍几个比较常用的命令,这些命令如表 15.1.4 所示: 表 15.1.4 SSD1306 常用命令表 第一个命令为 0X81,用于设置对比度的,这个命令包含了两个字节,第一个 0X81 为命令, 随后发送的一个字节为要设置的对比度的值。这个值设置得越大屏幕就越亮。 第二个命令为 0XAE/0XAF。0XAE 为关闭显示命令;0XAF 为开启显示命令。 第三个命令为 0X8D,该指令也包含 2 个字节,第一个为命令字,第二个为设置值,第二 个字节的 BIT2 表示电荷泵的开关状态,该位为 1,则开启电荷泵,为 0 则关闭。在模块初始化 的时候,这个必须要开启,否则是看不到屏幕显示的。 第四个命令为 0XB0~B7,该命令用于设置页地址,其低三位的值对应着 GRAM 的页地址。 215 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第五个指令为 0X00~0X0F,该指令用于设置显示时的起始列地址低四位。 第六个指令为 0X10~0X1F,该指令用于设置显示时的起始列地址高四位。 其他命令,我们就不在这里一一介绍了,大家可以参考 SSD1306 datasheet 的第 28 页。从 这页开始,对 SSD1306 的指令有详细的介绍。 最后,我们再来介绍一下 OLED 模块的初始化过程,SSD1306 的典型初始化框图如图 15.1.7 所示: 图 15.1.7 SSD1306 初始化框图 驱动 IC 的初始化代码,我们直接使用厂家推荐的设置就可以了,只要对细节部分进行一些 修改,使其满足我们自己的要求即可,其他不需要变动。 OLED 的介绍就到此为止,我们重点向大家介绍了 ALIENTEK OLED 模块的相关知识,接 下来我们将使用这个模块来显示字符和数字。通过以上介绍,我们可以得出 OLED 显示需要的 相关设置步骤如下: 1)设置 STM32 与 OLED 模块相连接的 IO。 这一步,先将我们与 OLED 模块相连的 IO 口设置为输出,具体使用哪些 IO 口,这里需要 根据连接电路以及 OLED 模块所设置的通讯模式来确定。这些将在硬件设计部分向大家介绍。 2)初始化 OLED 模块。 其实这里就是上面的初始化框图的内容,通过对 OLED 相关寄存器的初始化,来启动 OLED 的显示。为后续显示字符和数字做准备。 3)通过函数将字符和数字显示到 OLED 模块上。 这里就是通过我们设计的程序,将要显示的字符送到 OLED 模块就可以了,这些函数将在 软件设计部分向大家介绍。 通过以上三步,我们就可以使用 ALIENTEK OLED 模块来显示字符和数字了,在后面我们 还将会给大家介绍显示汉字的方法。这一部分就先介绍到这里。 216 15.2 硬件设计 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 本实验用到的硬件资源有: 1) 指示灯 DS0 2) OLED 模块 OLED 模块的电路在前面已有详细说明了,这里我们介绍 OLED 模块与 ALIETEK MiniSTM32 开发板的连接,MiniSTM32 开发板底板的 LCD 接口和 ALIENTEK OLED 模块直接 可以对插(靠左插!),连接如图 15.2.1 所示: 图 15.2.1 OLED 模块与开发板连接示意图 图中圈出来的部分就是连接 OLED 的接口,这里在硬件上,OLED 与 MiniSTM32 开发板 的 IO 口对应关系如下: OLED_CS 对应 PC9; OLED_RS 对应 PC8; OLED_WR 对应 PC7; OLED_RD 对应 PC6; OLED_D[7:0]对应 PB[7:0]; 这些线的连接,MiniSTM32 的内部已经连接好了,我们只需要将 OLED 模块插上去就好了。 实物连接如图 15.2.2 所示: 图 15.2.2 OLED 模块与开发板连接实物图 217 15.3 软件设计 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 我们直接打开 OLED 显示实验可以发现 HARDWARE 下面有一个 oled.c 文件,同时包含了 头文件 oled.h。oled.c 的代码,由于比较长,这里我们就不贴出来了,仅介绍几个比较重要的函 数。首先是 OLED_Init 函数,该函数的结构比较简单,开始是对 IO 口的初始化,这里我们用 了宏定义 OLED_MODE 来决定要设置的 IO 口,其他就是一些初始化序列了,我们按照厂家提 供的资料来做就可以。最后要说明一点的是,因为 OLED 是无背光的,在初始化之后,我们把 显存都清空了,所以我们在屏幕上是看不到任何内容的,跟没通电一个样,不要以为这就是初 始化失败,要写入数据模块才会显示的。OLED_Init 函数代码如下: //初始化 SSD1306 void OLED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOC, ENABLE ); #if OLED_MODE==1 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //使能 AFIO 时钟 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE); //JTAG-DP 失能 + SW-DP 使能 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2| GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_Write(GPIOB,0XFFFF); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC, GPIO_Pin_6|GPIO_Pin_7|GPIO_Pin_8|GPIO_Pin_9); //如果每一位决定一个 GPIO_Pin,则可以通过或的形式来初始化多个 IO #else GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD ; //推挽输出 GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_Write(GPIOB,0X03); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9; 218 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC, GPIO_Pin_8|GPIO_Pin_9); #endif OLED_WR_Byte(0xAE,OLED_CMD); //关闭显示 OLED_WR_Byte(0xD5,OLED_CMD); //设置时钟分频因子,震荡频率 OLED_WR_Byte(80,OLED_CMD); //[3:0],分频因子;[7:4],震荡频率 OLED_WR_Byte(0xA8,OLED_CMD); //设置驱动路数 OLED_WR_Byte(0X3F,OLED_CMD); //默认 0X3F(1/64) OLED_WR_Byte(0xD3,OLED_CMD); //设置显示偏移 OLED_WR_Byte(0X00,OLED_CMD); //默认为 0 OLED_WR_Byte(0x40,OLED_CMD); //设置显示开始行 [5:0],行数. OLED_WR_Byte(0x8D,OLED_CMD); //电荷泵设置 OLED_WR_Byte(0x14,OLED_CMD); //bit2,开启/关闭 OLED_WR_Byte(0x20,OLED_CMD); //设置内存地址模式 OLED_WR_Byte(0x02,OLED_CMD); // OLED_WR_Byte(0xA1,OLED_CMD); //段重定义设置,bit0:0,0->0;1,0->127; OLED_WR_Byte(0xC0,OLED_CMD); //设置 COM 扫描方向; OLED_WR_Byte(0xDA,OLED_CMD); //设置 COM 硬件引脚配置 OLED_WR_Byte(0x12,OLED_CMD); //[5:4]配置 OLED_WR_Byte(0x81,OLED_CMD); //对比度设置 OLED_WR_Byte(0xEF,OLED_CMD); //1~255;默认 0X7F (亮度设置,越大越亮) OLED_WR_Byte(0xD9,OLED_CMD); //设置预充电周期 OLED_WR_Byte(0xf1,OLED_CMD); //[3:0],PHASE 1;[7:4],PHASE 2; OLED_WR_Byte(0xDB,OLED_CMD); //设置 VCOMH 电压倍率 OLED_WR_Byte(0x30,OLED_CMD); //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc; OLED_WR_Byte(0xA4,OLED_CMD); //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏) OLED_WR_Byte(0xA6,OLED_CMD); //设置显示方式;bit0:1,反相显示;0,正常显示 OLED_WR_Byte(0xAF,OLED_CMD); //开启显示 OLED_Clear(); } 接着,要介绍的是 OLED_Refresh_Gram 函数。我们在 STM32 内部定义了一个块 GRAM: u8 OLED_GRAM[128][8];此部分 GRAM 对应 OLED 模块上的 GRAM。在操作的时候,我们只 要修改 STM32 内部的 GRAM 就可以了,然后通过 OLED_Refresh_Gram 函数把 GRAM 一次刷 新到 OLED 的 GRAM 上。该函数代码如下: 219 STM32 不完全手册(库函数版) //更新显存到 LCD ALIENTEK MiniSTM32 V3.0 开发板教程 void OLED_Refresh_Gram(void) { u8 i,n; for(i=0;i<8;i++) { OLED_WR_Byte (0xb0+i,OLED_CMD); //设置页地址(0~7) OLED_WR_Byte (0x00,OLED_CMD); //设置显示位置—列低地址 OLED_WR_Byte (0x10,OLED_CMD); //设置显示位置—列高地址 for(n=0;n<128;n++)OLED_WR_Byte(OLED_GRAM[n][i],OLED_DATA); } } OLED_Refresh_Gram 函数先设置页地址,然后写入列地址(也就是纵坐标),然后从 0 开 始写入 128 个字节,写满该页,最后循环把 8 页的内容都写入,就实现了整个从 STM32 显存 到 OLED 显存的拷贝。 OLED_Refresh_Gram 函数还用到了一个外部函数 ,也就是我们接着要介绍的函数: OLED_WR_Byte,该函数直接和硬件相关,函数代码如下: #if OLED_MODE==1 //向 SSD1306 写入一个字节。 //dat:要写入的数据/命令 //cmd:数据/命令标志 0,表示命令;1,表示数据; void OLED_WR_Byte(u8 dat,u8 cmd) { DATAOUT(dat); OLED_RS=cmd; OLED_CS=0; OLED_WR=0; OLED_WR=1; OLED_CS=1; OLED_RS=1; } #else //向 SSD1306 写入一个字节。 //dat:要写入的数据/命令 //cmd:数据/命令标志 0,表示命令;1,表示数据; void OLED_WR_Byte(u8 dat,u8 cmd) { u8 i; OLED_RS=cmd; //写命令 OLED_CS=0; for(i=0;i<8;i++) { OLED_SCLK=0; 220 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 if(dat&0x80)OLED_SDIN=1; else OLED_SDIN=0; OLED_SCLK=1; dat<<=1; } OLED_CS=1; OLED_RS=1; } #endif 这 里 有 2 个 一 样 的 函 数 , 通 过 宏 定 义 OLED_MODE 来 决 定 使 用 哪 一 个 。 如 果 OLED_MODE=1,就定义为并口模式,选择第一个函数,而如果为 0,则为 4 线串口模式,选 择第二个函数。这两个函数输入参数均为 2 个:dat 和 cmd,dat 为要写入的数据,cmd 则表明 该数据是命令还是数据。这两个函数的时序操作就是根据上面我们对 8080 接口以及 4 线 SPI 接口的时序来编写的。 OLED_GRAM[128][8]中的 128 代表列数(x 坐标),而 8 代表的是页,每页又包含 8 行, 总共 64 行(y 坐标)。从高到低对应行数从小到大。比如,我们要在 x=100,y=29 这个点写入 1,则可以用这个句子实现: OLED_GRAM[100][4]|=1<<2; 一个通用的在点(x,y)置 1 表达式为: OLED_GRAM[x][7-y/8]|=1<<(7-y%8); 其中 x 的范围为:0~127;y 的范围为:0~63。 因此,我们可以得出下一个将要介绍的函数:画点函数,void OLED_DrawPoint(u8 x,u8 y, u8 t);函数代码如下: void OLED_DrawPoint(u8 x,u8 y,u8 t) { u8 pos,bx,temp=0; if(x>127||y>63)return;//超出范围了. pos=7-y/8; bx=y%8; temp=1<<(7-bx); if(t)OLED_GRAM[x][pos]|=temp; else OLED_GRAM[x][pos]&=~temp; } 该函数有 3 个参数,前两个是坐标,第三个 t 为要写入 1 还是 0。该函数实现了我们在 OLED 模块上任意位置画点的功能。 接下来,我们介绍一下显示字符函数,OLED_ShowChar,在介绍之前,我们来介绍一下字 符(ASCII 字符集)是怎么显示在 OLED 模块上去的。要显示字符,我们先要有字符的点阵数 据,ASCII 常用的字符集总共有 95 个,从空格符开始,分别为: !"#$%&'()*+,-0123456789:;<= >?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~. 我们先要得到这个字符集的点阵数据,这里我们介绍一个款很好的字符提取软件: PCtoLCD2002 完美版。该软件可以提供各种字符,包括汉字(字体和大小都可以自己设置)阵 提取,且取模方式可以设置好几种,常用的取模方式,该软件都支持。该软件还支持图形模式, 也就是用户可以自己定义图片的大小,然后画图,根据所画的图形再生成点阵数据,这功能在 221 制作图标或图片的时候很有用。 该软件的界面如图 15.3.1 所示: STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 15.3.1 PCtoLCD2002 软件界面 然后我们选择设置,在设置里面设置取模方式如图 15.3.2 所示: 图 15.3.2 设置取模方式 上图设置的取模方式,在右上角的取模说明里面有,即:从第一列开始向下每取 8 个点作 为一个字节,如果最后不足 8 个点就补满 8 位。取模顺序是从高到低,即第一个点作为最高位。 如*-------取为 10000000。其实就是按如图 15.3.3 所示的这种方式: 222 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 15.3.3 取模方式图解 从上到下,从左到右,高位在前。我们按这样的取模方式,然后把 ASCII 字符集按 12*6 大小、16*8 和 24*12 大小取模出来(对应汉字大小为 12*12、16*16 和 24*24,字符的只有汉字 的一半大!),保存在 oledfont.h 里面,每个 12*6 的字符占用 12 个字节,每个 16*8 的字符占用 16 个字节,每个 24*12 的字符占用 36 个字节。具体见 oledfont.h 部分代码(该部分我们不再这 里列出来了,请大家参考光盘里面的代码)。 在知道了取模方式之后,我们就可以根据取模的方式来编写显示字符的代码了,这里我们 针对以上取模方式的显示字符代码如下: //在指定位置显示一个字符,包括部分字符 //x:0~127 //y:0~63 //mode:0,反白显示;1,正常显示 //size:选择字体 12/16/24 void OLED_ShowChar(u8 x,u8 y,u8 chr,u8 size,u8 mode) { u8 temp,t,t1; u8 y0=y; u8 csize=(size/8+((size%8)?1:0))*(size/2);//得到字体一个字符对应点阵集所占的字节数 chr=chr-' ';//得到偏移后的值 for(t=0;t'~')t=' '; delay_ms(500); LED0=!LED0; } } 该部分代码用于在 OLED 上显示一些字符,然后从空格键开始不停的循环显示 ASCII 字符 集,并显示该字符的 ASCII 值。然后我们编译此工程,直到编译成功为止。 15.4 下载验证 将代码下载到 MiniSTM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 OLED 模块显示如图 15.4.1 所示: 图 15.4.1 OLED 显示效果 225 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图中 OLED 显示了三种尺寸的字符:24*12(ALIENTEK)、16*8(0.96’ OLED TEST)和 12*6(剩下的内容)。说明我们的实验是成功的,实现了三种不同尺寸 ASCII 字符的显示,在 最后一行不停的显示 ASCII 字符以及其码值。 通过这一章的学习,我们学会了 ALIENTEK OLED 模块的使用,在调试代码的时候,又多 了一种显示信息的途径,在以后的程序编写中,大家可以好好利用。 226 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十六章 TFTLCD 显示实验 上一章我们介绍了 OLED 模块及其显示,但是该模块只能显示单色/双色,不能显示彩色, 而且尺寸也较小。本章我们将介绍 ALIENTEK 2.8 寸 TFT LCD 模块,该模块采用 TFTLCD 面 板,可以显示 16 位色的真彩图片。在本章中,我们将使用 MiniSTM32 开发板上的 LCD 接口, 来点亮 TFTLCD,并实现 ASCII 字符和彩色的显示等功能,并在串口打印 LCD 控制器 ID,同 时在 LCD 上面显示。本章分为如下几个部分: 16.1 TFTLCD 简介 16.2 硬件设计 16.3 软件设计 16.4 下载验证 227 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 16.1 TFTLCD 简介 本章我们将通过 STM32 的 FSMC 接口来控制 TFTLCD 的显示,所以本节分为两个部分, 分别介绍 TFTLCD 和 FSMC。 TFT-LCD 即薄膜晶体管液晶显示器。其英文全称为:Thin Film Transistor-Liquid Crystal Display。TFT-LCD 与无源 TN-LCD、STN-LCD 的简单矩阵不同,它在液晶显示屏的每一个象 素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特 性与扫描线数无关,因此大大提高了图像质量。TFT-LCD 也被叫做真彩液晶显示器。 上一章介绍了 OLED 模块,本章,我们给大家介绍 ALIENTEK TFTLCD 模块,该模块有 如下特点: 1,2.4’/2.8’/3.5’/4.3’/7’ 5 种大小的屏幕可选。 2,320×240 的分辨率(3.5’分辨率为:320*480,4.3’和 7’分辨率为:800*480)。 3,16 位真彩显示。 4,自带触摸屏,可以用来作为控制输入。 本章,我们以 2.8 寸的 ALIENTEK TFTLCD 模块为例介绍,该模块支持 65K 色显示,显示 分辨率为 320×240,接口为 16 位的 80 并口,自带触摸屏。 该模块的外观图如图 16.1.1 所示: 图 16.1.1 ALIENTEK 2.8 寸 TFTLCD 外观图 模块原理图如图 16.1.2 所示: 228 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 16.1.2 ALIENTEK 2.8 寸 TFTLCD 模块原理图 TFTLCD 模块采用 2*17 的 2.54 公排针与外部连接,接口定义如图 16.1.3 所示: 图 16.1.3 ALIENTEK 2.8 寸 TFTLCD 模块接口图 从图 16.1.3 可以看出,ALIENTEK TFTLCD 模块采用 16 位的并方式与外部连接,之所以 不采用 8 位的方式,是因为彩屏的数据量比较大,尤其在显示图片的时候,如果用 8 位数据线, 就会比 16 位方式慢一倍以上,我们当然希望速度越快越好,所以我们选择 16 位的接口。图 16.1.3 还列出了触摸屏芯片的接口,关于触摸屏本章我们不多介绍,后面的章节会有详细的介绍。该 模块的 80 并口有如下一些信号线: CS:TFTLCD 片选信号。 WR:向 TFTLCD 写入数据。 RD:从 TFTLCD 读取数据。 D[15:0]:16 位双向数据线。 RST:硬复位 TFTLCD。 RS:命令/数据标志(0,读写命令;1,读写数据)。 80 并口在上一节我们已经有详细的介绍了,这里我们就不再介绍,需要说明的是,TFTLCD 229 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 模块的 RST 信号线是直接接到 STM32 的复位脚上,并不由软件控制,这样可以省下来一个 IO 口。另外我们还需要一个背光控制线来控制 TFTLCD 的背光。所以,我们总共需要的 IO 口数 目为 21 个。这里还需要注意,我们标注的 DB1~DB8,DB10~DB17,是相对于 LCD 控制 IC 标 注的,实际上大家可以把他们就等同于 D0~D15(按从小到大顺序),这样理解起来简单点。 ALIENTEK 提 供 的 2.8 寸 TFTLCD 模 块 , 其 驱 动 芯 片 有 很 多 种 类 型 , 比 如 有 : ILI9341/ILI9325/RM68042/RM68021/ILI9320/ILI9328/LGDP4531/LGDP4535/SPFD5408/SSD128 9/1505/B505/C505/NT35310/NT35510 等(具体的型号,大家可以通过下载本章实验代码,通过串 口或者 LCD 显示查看),这里我们仅以 ILI9341 控制器为例进行介绍,其他的控制基本都类似, 我们就不详细阐述了。 ILI9341 液晶控制器自带显存,其显存总大小为 172800(240*320*18/8),即 18 位模式(26 万色)下的显存量。在 16 位模式下,ILI9341 采用 RGB565 格式存储颜色数据,此时 ILI9341 的 18 位数据线与 MCU 的 16 位数据线以及 LCD GRAM 的对应关系如图 16.1.4 所示: 图 16.1.4 16 位数据与显存对应关系图 从图中可以看出,ILI9341 在 16 位模式下面,数据线有用的是:D17~D13 和 D11~D1,D0 和 D12 没有用到,实际上在我们 LCD 模块里面,ILI9341 的 D0 和 D12 压根就没有引出来,这 样,ILI9341 的 D17~D13 和 D11~D1 对应 MCU 的 D15~D0。 这样 MCU 的 16 位数据,最低 5 位代表蓝色,中间 6 位为绿色,最高 5 位为红色。数值越 大,表示该颜色越深。另外,特别注意 ILI9341 所有的指令都是 8 位的(高 8 位无效),且参数 除了读写 GRAM 的时候是 16 位,其他操作参数,都是 8 位的,这个和 ILI9320 等驱动器不一 样,必须加以注意。 接下来,我们介绍一下 ILI9341 的几个重要命令,因为 ILI9341 的命令很多,我们这里就 不全部介绍了,有兴趣的大家可以找到 ILI9341 的 datasheet 看看。里面对这些命令有详细的介 绍。我们将介绍:0XD3,0X36,0X2A,0X2B,0X2C,0X2E 等 6 条指令。 首先来看指令:0XD3,这个是读 ID4 指令,用于读取 LCD 控制器的 ID,该指令如表 16.1.1 所示: 控制 顺序 RS RD WR D15~D8 各位描述 HEX D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 1 1 0 1 0 0 1 1 D3H 参数 1 1 ↑ 1 XX XXXXXXXX X 参数 2 1 ↑ 1 XX 0 0 0 0 0 0 0 0 00H 参数 3 1 ↑ 1 XX 1 0 0 1 0 0 1 1 93H 参数 4 1 ↑ 1 XX 0 1 0 0 0 0 0 1 41H 表 16.1.1 0XD3 指令描述 从上表可以看出,0XD3 指令后面跟了 4 个参数,最后 2 个参数,读出来是 0X93 和 0X41, 刚好是我们控制器 ILI9341 的数字部分,从而,通过该指令,即可判别所用的 LCD 驱动器是什 么型号,这样,我们的代码,就可以根据控制器的型号去执行对应驱动 IC 的初始化代码,从而 兼容不同驱动 IC 的屏,使得一个代码支持多款 LCD。 接下来看指令:0X36,这是存储访问控制指令,可以控制 ILI9341 存储器的读写方向,简 单的说,就是在连续写 GRAM 的时候,可以控制 GRAM 指针的增长方向,从而控制显示方式 230 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 (读 GRAM 也是一样)。该指令如表 16.1.2 所示: 控制 顺序 RS RD WR D15~D8 各位描述 HEX D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 0 0 1 1 0 1 1 0 36H 参数 1 1 ↑ XX MY MX MV ML BGR MH 0 0 0 表 16.1.2 0X36 指令描述 从上表可以看出,0X36 指令后面,紧跟一个参数,这里我们主要关注:MY、MX、MV 这三个位,通过这三个位的设置,我们可以控制整个 ILI9341 的全部扫描方向,如表 16.1.3 所 示: 控制位 效果 MY MX MV LCD 扫描方向(GRAM 自增方式) 000 从左到右,从上到下 100 010 110 从左到右,从下到上 从右到左,从上到下 从右到左,从下到上 001 从上到下,从左到右 011 从上到下,从右到左 101 从下到上,从左到右 111 从下到上,从右到左 表 16.1.3 MY、MX、MV 设置与 LCD 扫描方向关系表 这样,我们在利用 ILI9341 显示内容的时候,就有很大灵活性了,比如显示 BMP 图片, BMP 解码数据,就是从图片的左下角开始,慢慢显示到右上角,如果设置 LCD 扫描方向为从 左到右,从下到上,那么我们只需要设置一次坐标,然后就不停的往 LCD 填充颜色数据即可, 这样可以大大提高显示速度。 接下来看指令:0X2A,这是列地址设置指令,在从左到右,从上到下的扫描方式(默认) 下面,该指令用于设置横坐标(x 坐标),该指令如表 16.1.4 所示: 控制 各位描述 顺序 RS RD WR D15~D8 D7 D6 D5 D4 D3 D2 D1 D0 HEX 指令 0 1 ↑ XX 0 0 1 0 1 0 1 0 2AH 参数 1 1 1 ↑ XX SC15 SC14 SC13 SC12 SC11 SC10 SC9 SC8 SC 参数 2 1 1 ↑ XX SC7 SC6 SC5 SC4 SC3 SC2 SC1 SC0 参数 3 1 1 ↑ XX EC15 EC14 EC13 EC12 EC11 EC10 EC9 EC8 参数 4 1 1 ↑ XX EC7 EC6 EC5 EC4 EC3 EC2 EC1 EC0 EC 表 16.1.4 0X2A 指令描述 在默认扫描方式时,该指令用于设置 x 坐标,该指令带有 4 个参数,实际上是 2 个坐标值: SC 和 EC,即列地址的起始值和结束值,SC 必须小于等于 EC,且 0≤SC/EC≤239。一般在设 置 x 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SC 即可,因为如果 EC 没有变化, 我们只需要设置一次即可(在初始化 ILI9341 的时候设置),从而提高速度。 与 0X2A 指令类似,指令:0X2B,是页地址设置指令,在从左到右,从上到下的扫描方式 (默认)下面,该指令用于设置纵坐标(y 坐标)。该指令如表 16.1.5 所示: 231 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 顺序 控制 各位描述 HEX RS RD WR D15~D8 D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 0 0 1 0 1 0 1 0 2BH 参数 1 1 1 ↑ XX SP15 SP14 SP13 SP12 SP11 SP10 SP9 SP8 参数 2 1 1 ↑ XX SP7 SP6 SP5 SP4 SP3 SP2 SP1 SP0 SP 参数 3 1 1 ↑ XX EP15 EP14 EP13 EP12 EP11 EP10 EP9 EP8 EP 参数 4 1 1 ↑ XX EP7 EP6 EP5 EP4 EP3 EP2 EP1 EP0 表 16.1.5 0X2B 指令描述 在默认扫描方式时,该指令用于设置 y 坐标,该指令带有 4 个参数,实际上是 2 个坐标值: SP 和 EP,即页地址的起始值和结束值,SP 必须小于等于 EP,且 0≤SP/EP≤319。一般在设置 y 坐标的时候,我们只需要带 2 个参数即可,也就是设置 SP 即可,因为如果 EP 没有变化,我 们只需要设置一次即可(在初始化 ILI9341 的时候设置),从而提高速度。 接下来看指令:0X2C,该指令是写 GRAM 指令,在发送该指令之后,我们便可以往 LCD 的 GRAM 里面写入颜色数据了,该指令支持连续写,指令描述如表 16.1.6 所示: 顺序 控制 RS RD WR D15~D8 各位描述 HEX D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 0 0 1 0 1 1 0 0 2CH 参数 1 1 1 ↑ D1[15:0] XX …… 1 1 ↑ D2[15:0] XX 参数 n 1 1 ↑ Dn[15:0] XX 表 16.1.6 0X2C 指令描述 从上表可知,在收到指令 0X2C 之后,数据有效位宽变为 16 位,我们可以连续写入 LCD GRAM 值,而 GRAM 的地址将根据 MY/MX/MV 设置的扫描方向进行自增。例如:假设设置 的是从左到右,从上到下的扫描方式,那么设置好起始坐标(通过 SC,SP 设置)后,每写入 一个颜色值,GRAM 地址将会自动自增 1(SC++),如果碰到 EC,则回到 SC,同时 SP++,一 直到坐标:EC,EP 结束,其间无需再次设置的坐标,从而大大提高写入速度。 最后,来看看指令:0X2E,该指令是读 GRAM 指令,用于读取 ILI9341 的显存(GRAM), 该指令在 ILI9341 的数据手册上面的描述是有误的,真实的输出情况如表 16.1.7 所示: 顺序 控制 各位描述 HEX RS RD WR D15~D11 D10 D9 D8 D7 D6 D5 D4 D3 D2 D1 D0 指令 0 1 ↑ XX 0 0 1 0 1 1 1 0 2EH 参数 1 1 ↑ 1 XX dummy 参数 2 1 ↑ 1 R1[4:0] XX G1[5:0] XX R1G1 参数 3 1 ↑ 1 B1[4:0] XX R2[4:0] XX B1R2 参数 4 1 ↑ 1 G2[5:0] XX B2[4:0] XX G2B2 参数 5 1 ↑ 1 R3[4:0] XX G3[5:0] XX R3G3 参数 N 1 ↑ 1 按以上规律输出 表 16.1.7 0X2E 指令描述 该指令用于读取 GRAM,如表 16.1.7 所示,ILI9341 在收到该指令后,第一次输出的是 dummy 数据,也就是无效的数据,第二次开始,读取到的才是有效的 GRAM 数据(从坐标:SC,SP 开始),输出规律为:每个颜色分量占 8 个位,一次输出 2 个颜色分量。比如:第一次输出是 R1G1,随后的规律为:B1R2G2B2R3G3B3R4G4B4R5G5... 以此类推。如果我们只 需要读取一个点的颜色值,那么只需要接收到参数 3 即可,如果要连续读取(利用 GRAM 地址 232 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 自增,方法同上),那么就按照上述规律去接收颜色数据。 以上,就是操作 ILI9341 常用的几个指令,通过这几个指令,我们便可以很好的控制 ILI9341 显示我们所要显示的内容了。 一般 TFTLCD 模块的使用流程如图 16.1.4: 硬复位 LCD_RST=0; delay_ms(100); LCD_RST=1; 初始化序列 写GRAM指令 设置坐标 读GRAM指令 写入颜色数据 读出颜色数据 LCD显示 单片机处理 图 16.1.4 TFTLCD 使用流程 任何 LCD,使用流程都可以简单的用以上流程图表示。其中硬复位和初始化序列,只需要 执行一次即可。而画点流程就是:设置坐标写 GRAM 指令写入颜色数据,然后在 LCD 上 面,我们就可以看到对应的点显示我们写入的颜色了。读点流程为:设置坐标读 GRAM 指令 读取颜色数据,这样就可以获取到对应点的颜色数据了。 以上只是最简单的操作,也是最常用的操作,有了这些操作,一般就可以正常使用 TFTLCD 了。接下来我们将该模块用来来显示字符和数字,通过以上介绍,我们可以得出 TFTLCD 显示 需要的相关设置步骤如下: 1)设置 STM32 与 TFTLCD 模块相连接的 IO。 这一步,先将我们与 TFTLCD 模块相连的 IO 口进行初始化,以便驱动 LCD。这里需要根 据连接电路以及 TFTLCD 模块的设置来确定。 2)初始化 TFTLCD 模块。 即图 16.1.4 的初始化序列,这里我们没有硬复位 LCD,因为 MiniSTM32 开发板的 LCD 接 口,将 TFTLCD 的 RST 同 STM32 的 RESET 连接在一起了,只要按下开发板的 RESET 键,就 会对 LCD 进行硬复位。初始化序列,就是向 LCD 控制器写入一系列的设置值(比如伽马校准), 这些初始化序列一般 LCD 供应商会提供给客户,我们直接使用这些序列即可,不需要深入研究。 在初始化之后,LCD 才可以正常使用。 3)通过函数将字符和数字显示到 TFTLCD 模块上。 这一步则通过图 16.1.4 左侧的流程,即:设置坐标写 GRAM 指令写 GRAM 来实现, 但是这个步骤,只是一个点的处理,我们要显示字符/数字,就必须要多次使用这个步骤,从而 达到显示字符/数字的目标,所以需要设计一个函数来实现数字/字符的显示,之后调用该函数, 就可以实现数字/字符的显示了。 233 16.2 硬件设计 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 本实验用到的硬件资源有: 1) 指示灯 DS0 2) TFTLCD 模块 TFTLCD 模块的电路在前面已有详细说明了,这里我们介绍 TFTLCD 模块与 ALIETEK MiniSTM32 开发板的连接,MiniSTM32 开发板底板的 LCD 接口和 ALIENTEK TFTLCD 模块直 接可以对插(靠右插!),连接如图 16.2.1 所示: 图 16.2.1 TFTLCD 与开发板连接示意图 图 16.2.1 中圈出来的部分就是连接 TFTLCD 模块的接口,板上的接口比液晶模块的插针要 多 2 个口,液晶模块在这里是靠右插的。多出的 2 个口是给 OLED 用的,所以 OLED 模块在接 这里的时候,是靠左插的,这个要注意。在硬件上,TFTLCD 模块与 MiniSTM32 开发板的 IO 口对应关系如下: LCD_LED 对应 PC10; LCD_CS 对应 PC9; LCD _RS 对应 PC8; LCD _WR 对应 PC7; LCD _RD 对应 PC6; LCD _D[17:1]对应 PB[15:0]; 这些线的连接,MiniSTM32 开发板的内部已经连接好了,我们只需要将 TFTLCD 模块插 上去就好了。 16.3 软件设计 大家打开液晶显示实验工程会发现,我们在工程中添加了 lcd.c 文件和对应的头文件 lcd.h。 lcd.c 里面代码比较多,我们这里就不贴出来了,只针对几个重要的函数进行讲解。完整版 的代码见光盘4,程序源码标准例程-库函数版本实验 11 TFTLCD 显示实验的 lcd.c 文件。 首先,我们介绍一下 lcd.h 里面的一个重要结构体: //LCD 重要参数集 typedef struct { u16 width; u16 height; //LCD 宽度 //LCD 高度 234 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 u16 id; u8 dir; u16 wramcmd; u16 setxcmd; u16 setycmd; //LCD ID //横屏还是竖屏控制:0,竖屏;1,横屏。 //开始写 gram 指令 //设置 x 坐标指令 //设置 y 坐标指令 }_lcd_dev; //LCD 参数 extern _lcd_dev lcddev; //管理 LCD 重要参数 该结构体用于保存一些 LCD 重要参数信息,比如 LCD 的长宽、LCD ID(驱动 IC 型号)、 LCD 横竖屏状态等,这个结构体虽然占用了 14 个字节的内存,但是却可以让我们的驱动函数 支持不同尺寸的 LCD,同时可以实现 LCD 横竖屏切换等重要功能,所以还是利大于弊的。有 了以上了解,下面我们开始介绍 ILI93xx.c 里面的一些重要函数。 第一个是 LCD_WR_DATA 函数,该函数在 lcd.h 里面,通过宏定义的方式申明。该函数通 过 80 并口向 LCD 模块写入一个 16 位的数据,使用频率是最高的,这里我们采用了宏定义的方 式,以提高速度。其代码如下 //写数据函数 #define LCD_WR_DATA(data){\ LCD_RS_SET;\ LCD_CS_CLR;\ DATAOUT(data);\ LCD_WR_CLR;\ LCD_WR_SET;\ LCD_CS_SET;\ } 上面函数中的‘\’是 C 语言中的一个转义字符,用来连接上下文,因为宏定义只能是一个 串,而当你的串过长(超过一行的时候),就需要换行了,此时就必须通过反斜杠来连接上下文。 这里的‘\’正是起这个作用。在上面的函数中,LCD_RS_SET/ LCD_CS_CLR/ LCD_WR_CLR/ LCD_WR_SET/ LCD_CS_SET 等是操作 RS/CS/WR 的宏定义,均是采用 STM32 的快速 IO 控制 寄存器实现的,从而提高速度。 第二个是:LCD_WR_DATAX 函数,该函数在 ILI93xx.c 里面定义,功能和 LCD_WR_DATA 一模一样,该函数代码如下: //写数据函数 //可以替代 LCD_WR_DATAX 宏,拿时间换空间. //data:寄存器值 void LCD_WR_DATAX(u16 data) { LCD_RS_SET; LCD_CS_CLR; DATAOUT(data); LCD_WR_CLR; LCD_WR_SET; LCD_CS_SET; } 235 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 我们知道,宏定义函数的好处就是速度快(直接嵌到被调用函数里面去了),坏处就是占空 间大。在 LCD_Init 函数里面,有很多地方要写数据,如果全部用宏定义的 LCD_WR_DATA 函 数,那么就会占用非常大的 flash,所以我们这里另外实现一个函数:LCD_WR_DATAX,专门 给 LCD_Init 函数调用,从而大大减少 flash 占用量。 第三个是 LCD_WR_REG 函数,该函数是通过 8080 并口向 LCD 模块写入寄存器命令,因 为该函数使用频率不是很高,我们不采用宏定义来做(宏定义占用 FLASH 较多),通过 LCD_RS 来标记是写入命令(LCD_RS=0)还是数据(LCD_RS=1)。该函数代码如下: //写寄存器函数 //data:寄存器值 void LCD_WR_REG(u16 data) { LCD_RS_CLR;//写地址 LCD_CS_CLR; DATAOUT(data); LCD_WR_CLR; LCD_WR_SET; LCD_CS_SET; } 既然有写寄存器命令函数,那就有读寄存器数据函数。接下来介绍 LCD_RD_DATA 函数, 该函数用来读取 LCD 控制器的寄存器数据(非 GRAM 数据),该函数代码如下: //读 LCD 寄存器数据 //返回值:读到的值 u16 LCD_RD_DATA(void) { u16 t; GPIOB->CRL=0X88888888; //PB0-7 上拉输入 GPIOB->CRH=0X88888888; //PB8-15 上拉输入 GPIOB->ODR=0X0000; //全部输出 0 LCD_RS_SET; LCD_CS_CLR; LCD_RD_CLR; //读取数据(读寄存器时,并不需要读 2 次) if(lcddev.id==0X8989)delay_us(2);//FOR 8989,延时 2us t=DATAIN; LCD_RD_SET; LCD_CS_SET; GPIOB->CRL=0X33333333; //PB0-7 上拉输出 GPIOB->CRH=0X33333333; //PB8-15 上拉输出 GPIOB->ODR=0XFFFF; //全部输出高 return t; } 以上 4 个函数,用于实现 LCD 基本的读写操作,接下来,我们介绍 2 个 LCD 寄存器操作 的函数,LCD_WriteReg 和 LCD_ReadReg,这两个函数代码如下: //写寄存器 236 STM32 不完全手册(库函数版) //LCD_Reg:寄存器编号 //LCD_RegValue:要写入的值 ALIENTEK MiniSTM32 V3.0 开发板教程 void LCD_WriteReg(u16 LCD_Reg,u16 LCD_RegValue) { LCD_WR_REG(LCD_Reg); LCD_WR_DATA(LCD_RegValue); } //读寄存器 //LCD_Reg:寄存器编号 //返回值:读到的值 u16 LCD_ReadReg(u16 LCD_Reg) { LCD_WR_REG(LCD_Reg); //写入要读的寄存器号 return LCD_RD_DATA(); } 这两个函数函数十分简单,LCD_WriteReg 用于向 LCD 指定寄存器写入指定数据,而 LCD_ReadReg 则用于读取指定寄存器的数据,这两个函数,都只带一个参数/返回值,所以, 在有多个参数操作(读取/写入)的时候,就不适合用这两个函数了,得另外实现。 第七个要介绍的函数是坐标设置函数,该函数代码如下: //设置光标位置 //Xpos:横坐标 //Ypos:纵坐标 void LCD_SetCursor(u16 Xpos, u16 Ypos) { if(lcddev.id==0X9341||lcddev.id==0X5310) { LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(Xpos>>8); LCD_WR_DATA(Xpos&0XFF); LCD_WR_REG(lcddev.setycmd); LCD_WR_DATA(Ypos>>8); LCD_WR_DATA(Ypos&0XFF); }else if(lcddev.id==0X6804) { if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏时处理 LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(Xpos>>8); LCD_WR_DATA(Xpos&0XFF); LCD_WR_REG(lcddev.setycmd); LCD_WR_DATA(Ypos>>8); LCD_WR_DATA(Ypos&0XFF); }else if(lcddev.id==0X5510) { 237 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_WR_REG(lcddev.setxcmd); LCD_WR_DATA(Xpos>>8); LCD_WR_REG(lcddev.setxcmd+1); LCD_WR_DATA(Xpos&0XFF); LCD_WR_REG(lcddev.setycmd); LCD_WR_DATA(Ypos>>8); LCD_WR_REG(lcddev.setycmd+1); LCD_WR_DATA(Ypos&0XFF); }else { if(lcddev.dir==1)Xpos=lcddev.width-1-Xpos;//横屏其实就是调转 x,y 坐标 LCD_WriteReg(lcddev.setxcmd, Xpos); LCD_WriteReg(lcddev.setycmd, Ypos); } } 该函数实现将 LCD 的当前操作点设置到指定坐标(x,y)。因为不同 LCD 的设置方式不一定 完全一样,所以代码里面有好几个判断,对不同的驱动 IC 进行不同的设置。 接下来我们介绍第八个函数:画点函数。该函数实现代码如下: //画点 //x,y:坐标 //POINT_COLOR:此点的颜色 void LCD_DrawPoint(u16 x,u16 y) { LCD_SetCursor(x,y); //设置光标位置 LCD_WriteRAM_Prepare(); //开始写入 GRAM LCD_WR_DATA(POINT_COLOR); } 该函数实现比较简单,就是先设置坐标,然后往坐标写颜色。其中 POINT_COLOR 是我们 定义的一个全局变量,用于存放画笔颜色,顺带介绍一下另外一个全局变量:BACK_COLOR, 该变量代表 LCD 的背景色。LCD_DrawPoint 函数虽然简单,但是至关重要,其他几乎所有上 层函数,都是通过调用这个函数实现的。 有了画点,当然还需要有读点的函数,第九个介绍的函数就是读点函数,用于读取 LCD 的 GRAM,这里说明一下,为什么 OLED 模块没做读 GRAM 的函数,而这里做了。因为 OLED 模块是单色的,所需要全部 GRAM 也就 1K 个字节,而 TFTLCD 模块为彩色的,点数也比 OLED 模块多很多,以 16 位色计算,一款 320×240 的液晶,需要 320×240×2 个字节来存储颜色值, 也就是也需要 150K 字节,这对任何一款单片机来说,都不是一个小数目了。而且我们在图形 叠加的时候,可以先读回原来的值,然后写入新的值,在完成叠加后,我们又恢复原来的值。 这样在做一些简单菜单的时候,是很有用的。这里我们读取 TFTLCD 模块数据的函数为 LCD_ReadPoint,该函数直接返回读到的 GRAM 值。该函数使用之前要先设置读取的 GRAM 地址,通过 LCD_SetCursor 函数来实现。LCD_ReadPoint 的代码如下: //读取个某点的颜色值 //x,y:坐标 //返回值:此点的颜色 238 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 u16 LCD_ReadPoint(u16 x,u16 y) { u16 r,g,b; LCD_SetCursor(x,y); if(lcddev.id==0X9341||lcddev.id==0X6804||lcddev.id==0X5310)LCD_WR_REG(0X2E); //9341/6804/5310 发送读 GRAM 指令 else if(lcddev.id==0X5510)LCD_WR_REG(0X2E00);//5510 发送读 GRAM 指令 else LCD_WR_REG(R34); //其他 IC 发送读 GRAM 指令 GPIOB->CRL=0X88888888; //PB0-7 上拉输入 GPIOB->CRH=0X88888888; //PB8-15 上拉输入 GPIOB->ODR=0XFFFF; //全部输出高 LCD_RS_SET; LCD_CS_CLR; LCD_RD_CLR; delay_us(1); //延时 1us LCD_RD_SET; //读取数据(读 GRAM 时,第一次为假读) LCD_RD_CLR; delay_us(1); //延时 1us r=DATAIN; //实际坐标颜色 LCD_RD_SET; if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510)//这些要分 2 次读出 { LCD_RD_CLR; b=DATAIN;//读取蓝色值 LCD_RD_SET; g=r&0XFF;//对于 9341,第一次读取的是 RG 的值,R 在前,G 在后,各占 8 位 g<<=8; }else if(lcddev.id==0X6804) { LCD_RD_CLR; LCD_RD_SET; r=DATAIN;//6804 第二次读取的才是真实值 } LCD_CS_SET; GPIOB->CRL=0X33333333; GPIOB->CRH=0X33333333; GPIOB->ODR=0XFFFF; //PB0-7 上拉输出 //PB8-15 上拉输出 //全部输出高 if(lcddev.id==0X9325||lcddev.id==0X4535||lcddev.id==0X4531||lcddev.id==0X8989|| lcddev.id==0XB505)return r; //这几种 IC 直接返回颜色值 else if(lcddev.id==0X9341||lcddev.id==0X5310||lcddev.id==0X5510) return (((r>>11)<<11)|((g>>10)<<5)|(b>>11));//需要公式转换一下 else return LCD_BGR2RGB(r); //其他 IC } 在 LCD_ReadPoint 函数中,因为我们的代码不止支持一种 LCD 驱动器,所以,我们根据 不同的 LCD 驱动器((lcddev.id)型号,执行不同的操作,以实现对各个驱动器兼容,提高函数 的通用性。 239 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十个要介绍的是字符显示函数 LCD_ShowChar,该函数同前面 OLED 模块的字符显示函 数差不多,但是这里的字符显示函数多了一个功能,就是可以以叠加方式显示,或者以非叠加 方式显示。叠加方式显示多用于在显示的图片上再显示字符。非叠加方式一般用于普通的显示。 该函数实现代码如下: //在指定位置显示一个字符 //x,y:起始坐标 //num:要显示的字符:" "--->"~" //size:字体大小 12/16/24 //mode:叠加方式(1)还是非叠加方式(0) void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode) { u8 temp,t1,t; u16 y0=y; u8 csize=(size/8+((size%8)?1:0))*(size/2);//得到字体一个字符对应点阵集所占字节数 //设置窗口 num=num-' ';//得到偏移后的值 for(t=0;t=lcddev.width)return; //超区域了 if((y-y0)==size) { y=y0; x++; if(x>=lcddev.width)return; break; //超区域了 } } } } 在 LCD_ShowChar 函数里面,我们采用快速画点函数 LCD_Fast_DrawPoint 来画点显示字 符,该函数同 LCD_DrawPoint 一样,只是带了颜色参数,且减少了函数调用的时间,详见本例 程源码。该代码中我们用到了三个字符集点阵数据数组 asc2_2412、asc2_1206 和 asc2_1608, 这几个字符集的点阵数据的提取方式,同十五章介绍的方法是一模一样的。详细请参考第十五 章。 240 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 最后,我们再介绍一下 TFTLCD 模块的初始化函数 LCD_Init,该函数先初始化 STM32 与 TFTLCD 连接的 IO 口,并配置 FSMC 控制器,然后读取 LCD 控制器的型号,根据控制 IC 的 型号执行不同的初始化代码,其简化代码如下: //该初始化函数可以初始化各种 ALIENTEK 出品的 LCD 液晶屏 //本函数占用较大 flash,可根据自己的实际情况,删掉未用到的 LCD 初始化代码.以节省空间. void LCD_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC| RCC_APB2Periph_GPIOB|RCC_APB2Periph_AFIO, ENABLE); //使能 PORTB,C 时钟以及 AFIO 时钟 GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable , ENABLE); //开启 SWD GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_9| GPIO_Pin_8|GPIO_Pin_7|GPIO_Pin_6; // //PORTC6~10 复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); //GPIOC GPIO_SetBits(GPIOC,GPIO_Pin_10|GPIO_Pin_9|GPIO_Pin_8|GPIO_Pin_7|GPIO_Pin_6) ; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All; // PORTB 推挽输出 GPIO_Init(GPIOB, &GPIO_InitStructure); //GPIOB GPIO_SetBits(GPIOB,GPIO_Pin_All); delay_ms(50); // delay 50 ms LCD_WriteReg(0x0000,0x0001); //可以去掉 delay_ms(50); // delay 50 ms lcddev.id = LCD_ReadReg(0x0000); if(lcddev.id<0XFF||lcddev.id==0XFFFF||lcddev.id==0X9300)//读到 ID 不正确 { //尝试 9341 ID 的读取 LCD_WR_REG(0XD3); LCD_RD_DATA(); //dummy read LCD_RD_DATA(); //读到 0X00 lcddev.id=LCD_RD_DATA(); //读取 93 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA(); //读取 41 if(lcddev.id!=0X9341) //非 9341,尝试是不是 6804 { LCD_WR_REG(0XBF); LCD_RD_DATA(); //dummy read LCD_RD_DATA(); //读回 0X01 LCD_RD_DATA(); //读回 0XD0 lcddev.id=LCD_RD_DATA();//这里读回 0X68 241 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA();//这里读回 0X04 if(lcddev.id!=0X6804) //也不是 6804,尝试看看是不是 NT35310 { LCD_WR_REG(0XD4); LCD_RD_DATA(); LCD_RD_DATA(); lcddev.id=LCD_RD_DATA(); //dummy read //读回 0X01 //读回 0X53 lcddev.id<<=8; lcddev.id|=LCD_RD_DATA(); //这里读回 0X10 if(lcddev.id!=0X5310) //也不是 NT35310,尝试看看是不是 NT35510 { LCD_WR_REG(0XDA00); LCD_RD_DATA(); //读回 0X00 LCD_WR_REG(0XDB00); lcddev.id=LCD_RD_DATA();//读回 0X80 lcddev.id<<=8; LCD_WR_REG(0XDC00); lcddev.id|=LCD_RD_DATA();//读回 0X00 if(lcddev.id==0x8000)lcddev.id=0x5510; //NT35510 读回的 ID 是 8000H,为方便区分,我们强制设置为 5510 } } } } printf(" LCD ID:%x\r\n",lcddev.id); //打印 LCD ID if(lcddev.id==0X9341) //9341 初始化 { ……//9341 初始化代码 }else if(lcddev.id==0xXXXX) //其他 LCD 初始化代码 { ……//其他 LCD 驱动 IC,初始化代码 } LCD_Display_Dir(0); LCD_LED=1; //默认为竖屏显示 //点亮背光 LCD_Clear(WHITE); } 该函数先对 STM32 与 LCD 连接的相关 IO 进行初始化,之后读取 LCD 控制器型号(LCD ID), 根据读到的 LCD ID,对不同的驱动器执行不同的初始化代码,其中 else if(lcddev.id==0xXXXX), 是省略写法,实际上代码里面有十几个这种 else if 结构,从而可以支持十多款不同的驱动 IC 执 行初始化操作,这样大大提高了整个程序的通用性。大家在以后的学习中应该多使用这样的方 式,以提高程序的通用性、兼容性。 特别注意:本函数使用了 printf 来打印 LCD ID,所以,如果你在主函数里面没有初始化串 242 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 口,那么将导致程序死在 printf 里面!!如果不想用 printf,那么请注释掉它。 介绍完 lcd.c 文件后,接下来我们看看 lcd.h 文件内容: #ifndef __LCD_H #define __LCD_H #include "sys.h" #include "stdlib.h" //LCD 重要参数集 typedef struct { u16 width; u16 height; //LCD 宽度 //LCD 高度 u16 id; u8 dir; u16 wramcmd; u16 setxcmd; u16 setycmd; //LCD ID //横屏还是竖屏控制:0,竖屏;1,横屏。 //开始写 gram 指令 //设置 x 坐标指令 //设置 y 坐标指令 }_lcd_dev; //LCD 参数 extern _lcd_dev lcddev; //管理 LCD 重要参数 //LCD 的画笔颜色和背景色 extern u16 POINT_COLOR;//默认红色 extern u16 BACK_COLOR; //背景颜色.默认为白色 //LCD 端口定义,使用快速 IO 控制 #define LCD_LED PCout(10) //LCD 背光 PC10 #define LCD_CS_SET GPIO_SetBits(GPIOC,GPIO_Pin_9) //片选端口 #define LCD_RS_SET GPIO_SetBits(GPIOC,GPIO_Pin_8) //数据/命令 #define LCD_WR_SET GPIO_SetBits(GPIOC,GPIO_Pin_7) //写数据 #define LCD_RD_SET GPIO_SetBits(GPIOC,GPIO_Pin_6) //读数据 PC9 PC8 PC7 PC6 #define #define #define #define LCD_CS_CLR GPIO_ResetBits(GPIOC,GPIO_Pin_9) LCD_RS_CLR GPIO_ResetBits(GPIOC,GPIO_Pin_8) LCD_WR_CLR GPIO_ResetBits(GPIOC,GPIO_Pin_7) LCD_RD_CLR GPIO_ResetBits(GPIOC,GPIO_Pin_6) //片选端口 PC9 //数据/命令 PC8 //写数据 PC7 //读数据 PC6 //PB0~15,作为数据线 #define DATAOUT(x) GPIO_Write(GPIOB,x); #define DATAIN GPIO_ReadInputData(GPIOB) ////////////////////////////////////////////////////////////////////////////////// //扫描方向定义 #define L2R_U2D 0 //从左到右,从上到下 #define L2R_D2U 1 //从左到右,从下到上 #define R2L_U2D 2 //从右到左,从上到下 243 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define R2L_D2U 3 //从右到左,从下到上 #define U2D_L2R 4 //从上到下,从左到右 #define U2D_R2L 5 //从上到下,从右到左 #define D2U_L2R 6 //从下到上,从左到右 #define D2U_R2L 7 //从下到上,从右到左 #define DFT_SCAN_DIR L2R_U2D //默认的扫描方向 //画笔颜色 #define WHITE 0xFFFF ……//省略部分 #define LBBLUE 0X2B12 //浅棕蓝色(选择条目的反色) void LCD_Init(void); /初始化 void LCD_DisplayOn(void); //开显示 void LCD_DisplayOff(void); //关显示 void LCD_Clear(u16 Color); //清屏 void LCD_SetCursor(u16 Xpos, u16 Ypos); //设置光标 void LCD_DrawPoint(u16 x,u16 y); //画点 u16 LCD_ReadPoint(u16 x,u16 y); //读点 void Draw_Circle(u16 x0,u16 y0,u8 r); //画圆 void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2); //画线 void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2); //画矩形 void LCD_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 color); //填充单色 void LCD_Color_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 *color); //填充指定颜色 void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode); //显示一个字符 void LCD_ShowNum(u16 x,u16 y,u32 num,u8 len,u8 size); //显示一个数字 void LCD_ShowxNum(u16 x,u16 y,u32 num,u8 len,u8 size,u8 mode);//显示 数字 void LCD_ShowString(u16 x,u16 y,u16 width,u16 height,u8 size,u8 *p); //显示一个字符串,12/16/24 字体 void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue); u16 LCD_ReadReg(u8 LCD_Reg); void LCD_WriteRAM_Prepare(void); void LCD_WriteRAM(u16 RGB_Code); void LCD_Scan_Dir(u8 dir); //设置屏扫描方向 void LCD_Display_Dir(u8 dir); //设置屏幕显示方向 void LCD_Set_Window(u16 sx,u16 sy,u16 width,u16 height);//设置窗口 //9320/9325 LCD 寄存器 #define R0 ……//省略部分 0x00 #define R229 0xE5 #endif 代码里里面的_lcd_dev 结构体,在前面有已有介绍,其他的相对就比较简单了。另外这段 代码对颜色和驱动器的寄存器进行了很多宏定义,限于篇幅考虑,我们没有完全贴出来,省略 了其中绝大部分。此部分我们就不多说了。接下来,看看主函数代码: int main(void) 244 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { u8 x=0; u8 lcd_id[12]; delay_init(); uart_init(9600); LED_Init(); LCD_Init(); //存放 LCD ID 字符串 //延时函数初始化 //串口初始化为 9600 //初始化与 LED 连接的硬件接口 POINT_COLOR=RED; sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id); //将 LCD ID 打印到 lcd_id 数组。 while(1) { switch(x) { case 0:LCD_Clear(WHITE);break; case 1:LCD_Clear(BLACK);break; case 2:LCD_Clear(BLUE);break; case 3:LCD_Clear(RED);break; case 4:LCD_Clear(MAGENTA);break; case 5:LCD_Clear(GREEN);break; case 6:LCD_Clear(CYAN);break; case 7:LCD_Clear(YELLOW);break; case 8:LCD_Clear(BRRED);break; case 9:LCD_Clear(GRAY);break; case 10:LCD_Clear(LGRAY);break; case 11:LCD_Clear(BROWN);break; } POINT_COLOR=RED; LCD_ShowString(30,40,200,24,24,"Mini STM32 ^_^"); LCD_ShowString(30,70,200,16,16,"TFTLCD TEST") ; LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,lcd_id); //显示 LCD ID LCD_ShowString(30,130,200,12,12,"2014/3/7"); x++; if(x==12)x=0; LED0=!LED0; delay_ms(1000); } } 该部分代码将显示一些固定的字符,字体大小包括 24*12、16*8 和 12*6 等三种,同时显示 LCD 驱动 IC 的型号,然后不停的切换背景颜色,每 1s 切换一次。而 LED0 也会不停的闪烁, 指示程序已经在运行了。其中我们用到一个 sprintf 的函数,该函数用法同 printf,只是 sprintf 把打印内容输出到指定的内存区间上,sprintf 的详细用法,请百度。 另外特别注意:uart_init 函数,不能去掉,因为在 LCD_Init 函数里面调用了 printf,所以一 245 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 旦你去掉这个初始化,就会死机了。。。实际上,只要你的代码有用到 printf,就必须初始化串口, 否则都会死机,即停在 usart.c 里面的 fputc 函数,出不来。 在编译通过之后,我们开始下载验证代码。 16.4 下载验证 将程序下载到 MiniSTM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 TFTLCD 模块的显示如图 16.4.1 所示: 图 16.4.1 TFTLCD 显示效果图 我们可以看到屏幕的背景是不停切换的,同时 DS0 不停的闪烁,证明我们的代码被正确的 执行了,达到了我们预期的目的。 最后,再说明一下,这个 TFTLCD 例程支持 ALIENTEK 除 7 寸屏模块以外的其他所有 LCD 模块,自动兼容。比如:2.4 寸(320*240),2.8 寸(320*240),3.5 寸(480*320),4.3 寸(800*480) 等模块,直接插上去都是可以使用的。后续的例程,也都兼容这 4 种尺寸的 TFTLCD 模块,插 上去都是直接可以使用的。 246 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十七章 USMART 调试组件实验 本章,我们将向大家介绍一个十分重要的辅助调试工具:USMART 调试组件。该组件由 ALIENTEK 开发提供,功能类似 linux 的 shell(RTT 的 finsh 也属于此类)。USMART 最主要 的功能就是通过串口调用单片机里面的函数,并执行,对我们调试代码是很有帮助的。本章分 为如下几个部分: 17.1 USMART 调试组件简介 17.2 硬件设计 17.3 软件设计 17.4 下载验证 247 17.1 USMART 调试组件简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 USMART 是由 ALIENTEK 开发的一个灵巧的串口调试互交组件,通过它你可以通过串口 助手调用程序里面的任何函数,并执行。因此,你可以随意更改函数的输入参数(支持数字(10/16 进制)、字符串、函数入口地址等作为参数),单个函数最多支持 10 个输入参数,并支持函数返 回值显示,目前最新版本为 V3.1。 USMART 的特点如下: 1, 可以调用绝大部分用户直接编写的函数。 2, 资源占用极少(最少情况:FLASH:4K;SRAM:72B)。 3, 支持参数类型多(数字(包含 10/16 进制)、字符串、函数指针等)。 4, 支持函数返回值显示。 5, 支持参数及返回值格式设置。 6, 支持函数执行时间计算(V3.1 版本新特性)。 7, 使用方便。 有了 USMART,你可以轻易的修改函数参数、查看函数运行结果,从而快速分析解决问题。 比如你调试一个摄像头模块,需要修改其中的几个参数来得到最佳的效果,普通的做法:写函 数修改参数下载看结果不满意修改参数下载看结果不满意….不停的循环,直 到满意为止。这样做很麻烦不说,单片机也是有寿命的啊,老这样不停的刷,很折寿的。 如果利用 USMART,则只需要在串口调试助手里面输入函数及参数,然后直接串口发送给 单片机,就执行了一次参数调整,不满意的话,你在串口调试助手修改参数在发送就可以了, 直到你满意为止。这样,修改参数十分方便,不需要编译、不需要下载、不会让单片机折寿。 USMART 支持的参数类型基本满足任何调试了,支持的类型有:10 或者 16 进制数字、字 符串指针(如果该参数是用作参数返回的话,可能会有问题!)、函数指针等。因此绝大部分函 数,可以直接被 USMART 调用,对于不能直接调用的,你只需要重写一个函数,把影响调用 的参数去掉即可,这个重写后的函数,即可以被 USMART 调用了。 USMART 的实现流程简单概括就是:第一步,添加需要调用的函数(在 usmart_config.c 里 面的 usmart_nametab 数组里面添加);第二步,初始化串口;第三步,初始化 USMART(通过 usmart_init 函数实现);第四步,轮询 usmart_scan 函数,处理串口数据。 经过以上简单介绍,我们对 USMART 有了个大概了解,接下来我们来简单介绍下 USMART 组件的移植。 USMART 组件总共包含 6 文件如图 17.1.1 所示: 248 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 17.1.1 USMART 组件代码 其中 redeme.txt 是一个说明文件,不参与编译。其他五个文件,usmart.c 负责与外部互交等。 usmat_str.c 主要负责命令和参数解析。usmart_config.c 主要由用户添加需要由 usmart 管理的函 数。 usmart.h 和 usmart_str.h 是两个头文件,其中 usmart.h 里面含有几个用户配置宏定义,可以 用来配置 usmart 的功能及总参数长度(直接和 SRAM 占用挂钩)、是否使能定时器扫描、是否使 用读写函数等。 USMART 的移植,只需要实现 5 个函数。其中 4 个函数都在 usmart.c 里面,另外一个是串 口接收函数,必须由用户自己实现,用于接收串口发送过来的数据。 第一个函数,串口接收函数。该函数,我们是通过 SYSTEM 文件夹默认的串口接收来实现 的,该函数在 5.3.1 节有介绍过,我们这里就不列出来了。SYSTEM 文件夹里面的串口接收函 数,最大可以一次接收 200 字节,用于从串口接收函数名和参数等。大家如果在其他平台移植, 请参考 SYSTEM 文件夹串口接收的实现方式进行移植。 第二个是 void usmart_init(void)函数,该函数的实现代码如下: //初始化串口控制器 //sysclk:系统时钟(Mhz) void usmart_init(u8 sysclk) { #if USMART_ENTIMX_SCAN==1 Timer4_Init(1000,(u32)sysclk*100-1);//分频,时钟为 10K ,100ms 中断一次 //注意,计数频率必须为 10Khz,以和 runtime 的单位(0.1ms)同步. #endif usmart_dev.sptype=1; //十六进制显示参数 } 该函数有一个参数 sysclk,就是用于定时器初始化。另外 USMART_ENTIMX_SCAN 是在 usmart.h 里面定义的一个是否使能定时器中断扫描的宏定义。如果为 1,就初始化定时器中断, 249 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 并在中断里面调用 usmart_scan 函数。如果为 0,那么需要用户需要自行间隔一定时间(100ms 左右为宜)调用一次 usmart_scan 函数,以实现串口数据处理。注意:如果要使用函数执行时 间统计功能(指令:runtime 1),则必须设置 USMART_ENTIMX_SCAN 为 1。另外,为了让 统计时间精确到 0.1ms,定时器的计数时钟频率必须设置为 10Khz,否则时间就不是 0.1ms 了。 第三和第四个函数仅用于服务 USMART 的函数执行时间统计功能(串口指令:runtime 1), 分别是:usmart_reset_runtime 和 usmart_get_runtime,这两个函数代码如下: //复位 runtime //需要根据所移植到的 MCU 的定时器参数进行修改 void usmart_reset_runtime(void) { TIM4->SR&=~(1<<0); //清除中断标志位 TIM4->ARR=0XFFFF; //将重装载值设置到最大 TIM4->CNT=0; //清空定时器的 CNT usmart_dev.runtime=0; } //获得 runtime 时间 //返回值:执行时间,单位:0.1ms,最大延时时间为定时器 CNT 值的 2 倍*0.1ms //需要根据所移植到的 MCU 的定时器参数进行修改 u32 usmart_get_runtime(void) { if(TIM4->SR&0X0001)//在运行期间,产生了定时器溢出 { usmart_dev.runtime+=0XFFFF; } usmart_dev.runtime+=TIM4->CNT; return usmart_dev.runtime; //返回计数值 } 这里我们还是利用定时器 4 来做执行时间计算,usmart_reset_runtime 函数在每次 USMART 调用函数之前执行,清除计数器,然后在函数执行完之后,调用 usmart_get_runtime 获取整个 函数的运行时间。由于 usmart 调用的函数,都是在中断里面执行的,所以我们不太方便再用定 时器的中断功能来实现定时器溢出统计,因此,USMART 的函数执行时间统计功能,最多可以 统计定时器溢出 1 次的时间,对 STM32 来说,定时器是 16 位的,最大计数是 65535,而由于 我们定时器设置的是 0.1ms 一个计时周期(10Khz),所以最长计时时间是:65535*2*0.1ms=13.1 秒。也就是说,如果函数执行时间超过 13.1 秒,那么计时将不准确。 最后一个是 usmart_scan 函数,该函数用于执行 usmart 扫描,该函数需要得到两个参量, 第一个是从串口接收到的数组(USART_RX_BUF),第二个是串口接收状态(USART_RX_STA)。 接收状态包括接收到的数组大小,以及接收是否完成。该函数代码如下: //usmart 扫描函数 //通过调用该函数,实现 usmart 的各个控制.该函数需要每隔一定时间被调用一次 //以及时执行从串口发过来的各个函数. //本函数可以在中断里面调用,从而实现自动管理. //非 ALIENTEK 开发板用户,则 USART_RX_STA 和 USART_RX_BUF[]需要用户自己实现 void usmart_scan(void) 250 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { u8 sta,len; if(USART_RX_STA&0x8000)//串口接收完成? { len=USART_RX_STA&0x3fff; //得到此次接收到的数据长度 USART_RX_BUF[len]='\0'; //在末尾加入结束符. sta=usmart_dev.cmd_rec(USART_RX_BUF);//得到函数各个信息 if(sta==0)usmart_dev.exe();//执行函数 else { len=usmart_sys_cmd_exe(USART_RX_BUF); if(len!=USMART_FUNCERR)sta=len; if(sta) { switch(sta) { case USMART_FUNCERR: printf("函数错误!\r\n"); break; case USMART_PARMERR: printf("参数错误!\r\n"); break; case USMART_PARMOVER: printf("参数太多!\r\n"); break; case USMART_NOFUNCFIND: printf("未找到匹配的函数!\r\n"); break; } } } USART_RX_STA=0;//状态寄存器清空 } } 该函数的执行过程:先判断串口接收是否完成(USART_RX_STA 的最高位是否为 1),如 果完成,则取得串口接收到的数据长度(USART_RX_STA 的低 14 位),并在末尾增加结束符, 再执行解析,解析完之后清空接收标记(USART_RX_STA 置零)。如果没执行完成,则直接跳 过,不进行任何处理。 完成这几个函数的移植,你就可以使用 USMART 了。不过,需要注意的是,usmart 同外 部的互交,一般是通过 usmart_dev 结构体实现,所以 usmart_init 和 usmart_scan 的调用分别是 通过:usmart_dev.init 和 usmart_dev.scan 实现的。 下面,我们将在第十六章实验的基础上,移植 USMART,并通过 USMART 调用一些 TFTLCD 的内部函数,让大家初步了解 USMART 的使用。 251 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 17.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 和 DS1 2) 串口 3) TFTLCD 模块 这三个硬件在前面章节均有介绍,本章不再介绍。 17.3 软件设计 打开上一章的工程,复制 USMART 文件夹(该文件夹可以在:光盘标准例程-库函数版 本实验 12 USMART 调试组件实验 里面找到)到本工程文件夹下面,如图 17.3.1 所示: 图 17.3.1 复制 USMART 文件夹到工程文件夹下 图中的 keilkill.bat,是一个批处理文件,双击,可以删除 MDK 编译过程中产生的中间文件, 从而大大减少整个工程所占用的空间,节省硬盘空间,方便传输。 接着,我们打开工程,并新建 USMART 组,添加 USMART 组件代码,同时把 USMART 文件夹添加到头文件包含路径,在主函数里面加入 include“usmart.h”如图 17.3.2 所示: 252 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 17.3.2 添加 USMART 组件代码 由于 USMART 默认提供了 STM32 的 TIM4 中断初始化设置代码,我们只需要在 usmart.h 里面设置 USMART_ENTIMX_SCAN 为 1,即可完成 TIM4 的设置,通过 TIM4 的中断服务函 数,调用 usmart_dev.scan()(就是 usmart_scan 函数),实现 usmart 的扫描。此部分代码我们就 不列出来了,请参考 usmart.c。 此时,我们就可以使用 USMART 了,不过在主程序里面还得执行 usmart 的初始化,另外 还需要针对你自己想要被 USMART 调用的函数在 usmart_config.c 里面进行添加。下面先介绍 如何添加自己想要被 USMART 调用的函数,打开 usmart_config.c,如图 17.3.3 所示: 253 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 17.3.3 添加需要被 USMART 调用的函数 这里的添加函数很简单,只要把函数所在头文件添加进来,并把函数名按上图所示的方式 增加即可,默认我们添加了两个函数:delay_ms 和 delay_us。另外,read_addr 和 write_addr 属 于 usmart 自带的函数,用于读写指定地址的数据,通过配置 USMART_USE_WRFUNS,可以 使能或者禁止这两个函数。 这里我们根据自己的需要按上图的格式添加其他函数,添加完之后如图 17.3.4 所示: 254 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 17.3.4 添加函数后 上图中,我们添加了 lcd.h,并添加了很多 LCD 相关函数,注意,图中左侧有很多 MDK 动态语法检测的警告标志,我们不需要理会,这个编译完全是没有任何问题的。 最后我们还添加了 led_set 和 test_fun 两个函数,这两个函数在 test.c 里面实现,代码如下: //LED 状态设置函数 void led_set(u8 sta) { LED1=sta; } //函数参数调用测试函数 void test_fun(void(*ledset)(u8),u8 sta) { ledset(sta); } led_set 函数,用于设置 LED1 的状态,而第二个函数 test_fun 则是测试 USMART 对函数参 数的支持的,test_fun 的第一个参数是函数,在 USMART 里面也是可以被调用的。 在添加完函数之后,我们修改 main 函数,如下: int main(void) { delay_init(); //延时函数初始化 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 255 LCD_Init(); STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 //初始化 LCD usmart_dev.init(72); //初始化 USMART POINT_COLOR=RED; LCD_ShowString(30,50,200,16,16,"Mini STM32 ^_^"); LCD_ShowString(30,70,200,16,16,"USMART TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2014/3/8"); while(1) { LED0=!LED0; delay_ms(500); } } 此代码显示简单的信息后,就是在死循环等待串口数据。至此,整个 usmart 的移植就完成 了。编译成功后,就可以下载程序到开发板,开始 USMART 的体验。 17.4 下载验证 将程序下载到 MiniSTM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时, 屏幕上显示了一些字符(就是主函数里面要显示的字符)。 我们打开串口调试助手 XCOM,选择正确的串口号多条发送勾选发送新行(即发送回 车键)选项,然后发送 list 指令,即可打印所有 usmart 可调用函数。如下图所示: 图 17.4.1 驱动串口调试助手 上图中 list、id、help、hex、dec 和 runtime 都属于 usmart 自带的系统命令,点击后方的数 字按钮,即可发送对应的指令。下面我们简单介绍下这几个命令: 256 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 list,该命令用于打印所有 usmart 可调用函数。发送该命令后,串口将受到所有能被 usmart 调用得到函数,如图 17.4.1 所示。 id,该指令用于获取各个函数的入口地址。比如前面写的 test_fun 函数,就有一个函数参数, 我们需要先通过 id 指令,获取 ledset 函数的 id(即入口地址),然后将这个 id 作为函数参数, 传递给 test_fun。 help(或者‘ ?’也可以),发送该指令后,串口将打印 usmart 使用的帮助信息。 hex 和 dec,这两个指令可以带参数,也可以不带参数。当不带参数的时候,hex 和 dec 分 别用于设置串口显示数据格式为 16 进制/10 进制。当带参数的时候,hex 和 dec 就执行进制转 换,比如输入:hex 1234,串口将打印:HEX:0X4D2,也就是将 1234 转换为 16 进制打印出来。 又比如输入:dec 0X1234,串口将打印:DEC:4660,就是将 0X1234 转换为 10 进制打印出来。 runtime 指令,用于函数执行时间统计功能的开启和关闭,发送:runtime 1,可以开启函数 执行时间统计功能;发送:runtime 0,可以关闭函数执行时间统计功能。函数执行时间统计功 能,默认是关闭的。 大家可以亲自体验下这几个系统指令,不过要注意,所有的指令都是大小写敏感的,不要 写错哦。 接下来,我们将介绍如何调用 list 所打印的这些函数,先来看一个简单的 delay_ms 的调用, 我们分别输入 delay_ms(1000)和 delay_ms(0x3E8),如图 17.4.2 所示: 图 17.4.2 串口调用 delay_ms 函数 从上图可以看出,delay_ms(1000)和 delay_ms(0x3E8)的调用结果是一样的,都是延时 1000ms,因为 usmart 默认设置的是 hex 显示,所以看到串口打印的参数都是 16 进制格式的, 大家可以通过发送 dec 指令切换为十进制显示。另外,由于 USMART 对调用函数的参数大小写 不敏感,所以参数写成:0X3E8 或者 0x3e8 都是正确的。另外,发送:runtime 1,开启运行时 间统计功能,从测试结果看,USMART 的函数运行时间统计功能,是相当准确的。 我们再看另外一个函数,LCD_ShowString 函数,该函数用于显示字符串,我们通过串口输 入:LCD_ShowString(20,200,200,100,16,"This is a test for usmart!!"),如图 17.4.3 所示: 257 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 17.4.3 串口调用 LCD_ShowString 函数 该函数用于在指定区域,显示指定字符串,发送给开发板后,我们可以看到 LCD 在我们指 定的地方显示了:This is a test for usmart!! 这个字符串。 其他函数的调用,也都是一样的方法,这里我们就不多介绍了,最后说一下带有函数参数 的函数的调用。我们将 led_set 函数作为 test_fun 的参数,通过在 test_fun 里面调用 led_set 函数, 实现对 DS1(LED1)的控制。前面说过,我们要调用带有函数参数的函数,就必须先得到函数参 数的入口地址(id),通过输入 id 指令,我们可以得到 led_set 的函数入口地址是:0X0800022D, 所以,我们在串口输入:test_fun(0X0800022D,0),就可以控制 DS1 亮了。如图 17.4.4 所示: 258 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 17.4.4 串口调用 test_fun 函数 在开发板上,我们可以看到,收到串口发送的 test_fun(0X0800592D,0)后,开发板的 DS1 亮了,然后大家可以通过发送 test_fun(0X0800592D,1),来关闭 DS1。说明我们成功的通过 test_fun 函数调用 led_set,实现了对 DS1 的控制。也就验证了 USMART 对函数参数的支持。 USMART 调试组件的使用,就为大家介绍到这里。USMART 是一个非常不错的调试组件, 希望大家能学会使用,可以达到事半功倍的效果。 259 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十八章 RTC 实时时钟实验 前面我们介绍了两款液晶模块,这一章我们将介绍 STM32 的内部实时时钟(RTC)。在本 章中,我们将使用 ALIENTEK 2.8 寸 TFTLCD 模块来显示日期和时间,实现一个简单的时钟。 另外,本章将顺带向大家介绍 BKP 的使用。本章分为如下几个部分: 18.1 STM32 RTC 时钟简介 18.2 硬件设计 18.3 软件设计 18.4 下载验证 260 18.1 STM32 RTC 时钟简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 STM32 的实时时钟(RTC)是一个独立的定时器。STM32 的 RTC 模块拥有一组连续计数 的计数器,在相应软件配置下,可提供时钟日历的功能。修改计数器的值可以重新设置系统当 前的时间和日期。 RTC 模块和时钟配置系统(RCC_BDCR 寄存器)是在后备区域,即在系统复位或从待机模式 唤醒后 RTC 的设置和时间维持不变。但是在系统复位后,会自动禁止访问后备寄存器和 RTC, 以防止对后备区域(BKP)的意外写操作。所以在要设置时间之前, 先要取消备份区域(BKP) 写保护。 RTC 的简化框图,如图 18.1.1 所示: 图 18.1.1 RTC 框图 RTC 由两个主要部分组成(参见图 18.1.1),第一部分(APB1 接口)用来和 APB1 总线相连。 此单元还包含一组 16 位寄存器,可通过 APB1 总线对其进行读写操作。APB1 接口由 APB1 总 线时钟驱动,用来与 APB1 总线连接。 另一部分(RTC 核心)由一组可编程计数器组成,分成两个主要模块。第一个模块是 RTC 的 预分频模块,它可编程产生 1 秒的 RTC 时间基准 TR_CLK。RTC 的预分频模块包含了一个 20 位的可编程分频器(RTC 预分频器)。如果在 RTC_CR 寄存器中设置了相应的允许位,则在每个 TR_CLK 周期中 RTC 产生一个中断(秒中断)。第二个模块是一个 32 位的可编程计数器,可被 初始化为当前的系统时间,一个 32 位的时钟计数器,按秒钟计算,可以记录 4294967296 秒, 约合 136 年左右,作为一般应用,这已经是足够了的。 RTC 还有一个闹钟寄存器 RTC_ALR,用于产生闹钟。系统时间按 TR_CLK 周期累加并与 261 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 存储在 RTC_ALR 寄存器中的可编程时间相比较,如果 RTC_CR 控制寄存器中设置了相应允许 位,比较匹配时将产生一个闹钟中断。 RTC 内核完全独立于 RTC APB1 接口,而软件是通过 APB1 接口访问 RTC 的预分频值、计 数器值和闹钟值的。但是相关可读寄存器只在 RTC APB1 时钟进行重新同步的 RTC 时钟的上升 沿被更新,RTC 标志也是如此。这就意味着,如果 APB1 接口刚刚被开启之后,在第一次的内 部寄存器更新之前,从 APB1 上读取的 RTC 寄存器值可能被破坏了(通常读到 0)。因此,若 在读取 RTC 寄存器曾经被禁止的 RTC APB1 接口,软件首先必须等待 RTC_CRL 寄存器的 RSF 位(寄存器同步标志位,bit3)被硬件置 1。 接下来,我们介绍一下 RTC 相关的几个寄存器。首先要介绍的是 RTC 的控制寄存器,RTC 总共有 2 个控制寄存器 RTC_CRH 和 RTC_CRL,两个都是 16 位的。RTC_CRH 的各位描如图 18.1.2 所示: 图 18.1.2 RTC_CRH 寄存器各位描述 该寄存器用来控制中断的,我们本章将要用到秒钟中断,所以在该寄存器必须设置最低位 为 1,以允许秒钟中断。我们再看看 RTC_CRL 寄存器。该寄存器各位描述如图 18.1.3 所示: 262 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 18.1.3 RTC_CRL 寄存器各位描述 本章我们用到的是该寄存器的 0、3~5 这几个位,第 0 位是秒钟标志位,我们在进入闹钟 中断的时候,通过判断这位来决定是不是发生了秒钟中断。然后必须通过软件将该位清零(写 0)。第 3 位为寄存器同步标志位,我们在修改控制寄存器 RTC_CRH/CRL 之前,必须先判断该 位,是否已经同步了,如果没有则等待同步,在没同步的情况下修改 RTC_CRH/CRL 的值是不 行的。第 4 位为配置标位,在软件修改 RTC_CNT/RTC_ALR/RTC_PRL 的值的时候,必须先软 件置位该位,以允许进入配置模式。第 5 位为 RTC 操作位,该位由硬件操作,软件只读。通过 该位可以判断上次对 RTC 寄存器的操作是否完成,如果没有,我们必须等待上一次操作结束才 能开始下一次操作。 第二个要介绍的寄存器是 RTC 预分频装载寄存器,也有 2 个寄存器组成,RTC_PRLH 和 RTC_PRLL。这两个寄存器用来配置 RTC 时钟的分频数的,比如我们使用外部 32.768K 的晶振 作为时钟的输入频率,那么我们要设置这两个寄存器的值为 32767,以得到一秒钟的计数频率。 263 RTC_PRLH 的各位描述如图 18.1.4 所示: STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 18.1.4 RTC_PRLH 寄存器各位描述 从图 18.1.4 可以看出,RTC_PRLH 只有低四位有效,用来存储 PRL 的 19~16 位。而 PRL 的前 16 位,存放在 RTC_PRLL 里面,寄存器 RTC_PRLL 的各位描述如图 18.1.5 所示: 图 18.1.5 RTC_PRLL 寄存器各位描述 在介绍完这两个寄存器之后,我们介绍 RTC 预分频器余数寄存器,该寄存器也有 2 个寄存 器组成 RTC_DIVH 和 RTC_DIVL,这两个寄存器的作用就是用来获得比秒钟更为准确的时钟, 比如可以得到 0.1 秒,或者 0.01 秒等。该寄存器的值自减的,用于保存还需要多少时钟周期获 得一个秒信号。在一次秒钟更新后,由硬件重新装载。这两个寄存器和 RTC 预分频装载寄存器 的各位是一样的,这里我们就不列出来了。 接着要介绍的是 RTC 最重要的寄存器,RTC 计数器寄存器 RTC_CNT。该寄存器由 2 个 16 位的寄存器组成 RTC_CNTH 和 RTC_CNTL,总共 32 位,用来记录秒钟值(一般情况下)。此 两个计数器也比较简单,我们也不多说了。注意一点,在修改这个寄存器的时候要先进入配置 模式。 最后我们介绍 RTC 部分的最后一个寄存器,RTC 闹钟寄存器,该寄存器也是由 2 个 16 为 的寄存器组成 RTC_ALRH 和 RTC_ALRL。总共也是 32 位,用来标记闹钟产生的时间(以秒为 单位),如果 RTC_CNT 的值与 RTC_ALR 的值相等,并使能了中断的话,会产生一个闹钟中断。 该寄存器的修改也要进入配置模式才能进行。 因为我们使用到备份寄存器来存储 RTC 的相关信息(我们这里主要用来标记时钟是否已经 经过了配置),我们这里顺便介绍一下 STM32 的备份寄存器。 备份寄存器是 42 个 16 位的寄存器(Mini 开发板就是大容量的),可用来存储 84 个字节的 用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。 即使系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。 此外,BKP 控制寄存器用来管理侵入检测和 RTC 校准功能,这里我们不作介绍。 复位后,对备份寄存器和 RTC 的访问被禁止,并且备份域被保护以防止可能存在的意外的 写操作。执行以下操作可以使能对备份寄存器和 RTC 的访问: 1)通过设置寄存器 RCC_APB1ENR 的 PWREN 和 BKPEN 位来打开电源和后备接口的时 钟 2)电源控制寄存器(PWR_CR)的 DBP 位来使能对后备寄存器和 RTC 的访问。 264 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 我们一般用 BKP 来存储 RTC 的校验值或者记录一些重要的数据,相当于一个 EEPROM, 不过这个 EEPROM 并不是真正的 EEPROM,而是需要电池来维持它的数据。关于 BKP 的详细 介绍请看《STM32 参考手册》的第 47 页,5.1 一节。 最后,我们还要介绍一下备份区域控制寄存器 RCC_BDCR。该寄存器的个位描述如图 18.1.6 所示: 图 18.1.6 RCC_ BDCR 寄存器各位描述 RTC 的时钟源选择及使能设置都是通过这个寄存器来实现的,所以我们在 RTC 操作之前 先要通过这个寄存器选择 RTC 的时钟源,然后才能开始其他的操作。 寄存器介绍就给大家介绍到这里了,我们下面来看看要经过哪几个步骤的配置才能使 RTC 正常工作,这里我们将对每个步骤通过库函数的实现方式来讲解。 RTC 相关的库函数在文件 stm32f10x_rtc.c 和 stm32f10x_rtc.h 文件中,BKP 相关的库函数在 265 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 文件 stm32f10x_bkp.c 和文件 stm32f10x_bkp.h 文件中。 RTC 正常工作的一般配置步骤如下: 1)使能电源时钟和备份区域时钟。 前面已经介绍了,我们要访问 RTC 和备份区域就必须先使能电源时钟和备份区域时钟。 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); 2)取消备份区写保护。 要向备份区域写入数据,就要先取消备份区域写保护(写保护在每次硬复位之后被使能), 否则是无法向备份区域写入数据的。我们需要用到向备份区域写入一个字节,来标记时钟已经 配置过了,这样避免每次复位之后重新配置时钟。取消备份区域写保护的库函数实现方法是: PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问 3)复位备份区域,开启外部低速振荡器。 在取消备份区域写保护之后,我们可以先对这个区域复位,以清除前面的设置,当然这个 操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要 看情况而定。然后我们使能外部低速振荡器,注意这里一般要先判断 RCC_BDCR 的 LSERDY 位来确定低速振荡器已经就绪了才开始下面的操作。 备份区域复位的函数是: BKP_DeInit();//复位备份区域 开启外部低速振荡器的函数是: RCC_LSEConfig(RCC_LSE_ON);// 开启外部低速振荡器 4)选择 RTC 时钟,并使能。 这里我们将通过 RCC_BDCR 的 RTCSEL 来选择选择外部 LSI 作为 RTC 的时钟。然后通过 RTCEN 位使能 RTC 时钟。 库函数中,选择 RTC 时钟的函数是: RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择 LSE 作为 RTC 时钟 对于 RTC 时钟的选择,还有 RCC_RTCCLKSource_LSI 和 RCC_RTCCLKSource_HSE_Div128 两个,顾名思义,前者为 LSI,后者为 HSE 的 128 分频,这在时钟系统章节有讲解过。 使能 RTC 时钟的函数是: RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟 5)设置 RTC 的分频,以及配置 RTC 时钟。 在开启了 RTC 时钟之后,我们要做的就是设置 RTC 时钟的分频数,通过 RTC_PRLH 和 RTC_PRLL 来设置,然后等待 RTC 寄存器操作完成,并同步之后,设置秒钟中断。然后设置 RTC 的允许配置位(RTC_CRH 的 CNF 位),设置时间(其实就是设置 RTC_CNTH 和 RTC_CNTL 两个寄存器)。下面我们一一这些步骤用到的库函数: 在进行 RTC 配置之前首先要打开允许配置位(CNF),库函数是: RTC_EnterConfigMode();/// 允许配置 在配置完成之后,千万别忘记更新配置同时退出配置模式,函数是: RTC_ExitConfigMode();//退出配置模式,更新配置 设置 RTC 时钟分频数,库函数是: void RTC_SetPrescaler(uint32_t PrescalerValue); 这个函数只有一个入口参数,就是 RTC 时钟的分频数,很好理解。 然后是设置秒中断允许,RTC 使能中断的函数是: void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); 这个函数的第一个参数是设置秒中断类型,这些通过宏定义定义的。对于使能秒中断方法是: 266 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 下一步便是设置时间了,设置时间实际上就是设置 RTC 的计数值,时间与计数值之间是需要换 算的。库函数中设置 RTC 计数值的方法是: void RTC_SetCounter(uint32_t CounterValue)最后在配置完成之后 通过这个函数直接设置 RTC 计数值。 6)更新配置,设置 RTC 中断分组。 在设置完时钟之后,我们将配置更新同时退出配置模式,这里还是通过 RTC_CRH 的 CNF 来实现。库函数的方法是: RTC_ExitConfigMode();//退出配置模式,更新配置 在退出配置模式更新配置之后我们在备份区域 BKP_DR1 中写入 0X5050 代表我们已经初始化 过时钟了,下次开机(或复位)的时候,先读取 BKP_DR1 的值,然后判断是否是 0X5050 来 决定是不是要配置。接着我们配置 RTC 的秒钟中断,并进行分组。 往备份区域写用户数据的函数是: void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data); 这个函数的第一个参数就是寄存器的标号了,这个是通过宏定义定义的。比如我们要往 BKP_DR1 写入 0x5050,方法是: BKP_WriteBackupRegister(BKP_DR1, 0X5050); 同时,有写便有读,读取备份区域指定寄存器的用户数据的函数是: uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR); 这个函数就很好理解了,这里不做过多讲解。 设置中断分组的方法之前已经详细讲解过,调用 NVIC_Init 函数即可,这里不做重复讲解。 7)编写中断服务函数。 最后,我们要编写中断服务函数,在秒钟中断产生的时候,读取当前的时间值,并显示到 TFTLCD 模块上。 通过以上几个步骤,我们就完成了对 RTC 的配置,并通过秒钟中断来更新时间。接下来我 们将进行下一步的工作。 18.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 串口 3) TFTLCD 模块 4) RTC 前面 3 个都介绍过了,而 RTC 属于 STM32 内部资源,其配置也是通过软件设置好就可以 了。不过 RTC 不能断电,否则数据就丢失了,我们如果想让时间在断电后还可以继续走,那么 必须确保开发板的电池有电(ALIENTEK MiniSTM32 开发板标配是有电池的)。 18.3 软件设计 同样,打开我们光盘的 RTC 时钟实验,可以看到,我们的工程中加入了 rtc.c 源文件和 rtc.h 头文件,同时,引入了 stm32f10x_rtc.c 和 stm32f10x_bkp.c 库文件。 由于篇幅所限,rtc.c 中的代码,我们不全部贴出了,这里针对几个重要的函数,进行简要 说明,首先是 RTC_Init,其代码如下: //实时时钟配置 267 STM32 不完全手册(库函数版) //初始化 RTC 时钟,同时检测时钟是否工作正常 //BKP->DR1 用于保存是否第一次配置的设置 //返回 0:正常 //其他:错误代码 ALIENTEK MiniSTM32 V3.0 开发板教程 u8 RTC_Init(void) { u8 temp=0; //检查是不是第一次配置时钟 if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050) //从指定的后备寄存器中 //读出数据:读出了与写入的指定数据不相乎 { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); //使能 PWR 和 BKP 外设时钟 PWR_BackupAccessCmd(ENABLE); //使能后备寄存器访问 BKP_DeInit(); //③复位备份区域 RCC_LSEConfig(RCC_LSE_ON); //设置外部低速晶振(LSE) while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET) //检查指定的 //RCC 标志位设置与否,等待低速晶振就绪 { temp++; delay_ms(10); } if(temp>=250)return 1;//初始化时钟失败,晶振有问题 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置 RTC 时钟 //(RTCCLK),选择 LSE 作为 RTC 时钟 RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_WaitForSynchro(); //等待 RTC 寄存器同步 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_EnterConfigMode(); // 允许配置 RTC_SetPrescaler(32767); //设置 RTC 预分频的值 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_Set(2009,12,2,10,0,55); //设置时间 RTC_ExitConfigMode(); //退出配置模式 BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中 //写入用户程序数据 0x5050 } else//系统继续计时 { RTC_WaitForSynchro(); //等待最近一次对 RTC 寄存器的写操作完成 RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断 RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成 268 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 } RTC_NVIC_Config(); RTC_Get(); return 0; //RCT 中断分组设置 //更新时间 //ok } 该函数用来初始化 RTC 时钟,但是只在第一次的时候设置时间,以后如果重新上电/复位 都不会再进行时间设置了(前提是备份电池有电),在第一次配置的时候,我们是按照上面介绍 的 RTC 初始化步骤来做的,这里就不在多说了,这里我们设置时间是通过时间设置函数 RTC_Set(2014,3,8,22,10,55);来实现的,这里我们默认将时间设置为 2014 年 3 月 8 日 22 点 10 分 55 秒。在设置好时间之后,我们通过语句 BKP_WriteBackupRegister(BKP_DR1, 0X5050);向 BKP->DR1 写入标志字 0X5050,用于标记时间已经被设置了。这样,再次发生复位的时候,该 函数通过语句 if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)判断 BKP->DR1 的值,来决定 是不是需要重新设置时间,如果不需要设置,则跳过时间设置,仅仅使能秒钟中断一下,就进 行中断分组,然后返回了。这样不会重复设置时间,使得我们设置的时间不会因复位或者断电 而丢失。 该函数还有返回值,返回值代表此次操作的成功与否,如果返回 0,则代表初始化 RTC 成 功,如果返回值非零则代表错误代码了。 介绍完 RTC_Init,我们来介绍一下 RTC_Set 函数,该函数代码如下: //设置时钟 //把输入的时钟转换为秒钟 //以 1970 年 1 月 1 日为基准 //1970~2099 年为合法年份 //返回值:0,成功;其他:错误代码. //平年的月份日期表 const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31}; //syear,smon,sday,hour,min,sec:年月日时分秒 //返回值:设置结果。0,成功;1,失败。 u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec) { u16 t; u32 seccount=0; if(syear<1970||syear>2099)return 1; for(t=1970;t=365) { if(Is_Leap_Year(temp1))//是闰年 { if(temp>=366)temp-=366;//闰年的秒钟数 else break; } else temp-=365; temp1++; //平年 } calendar.w_year=temp1;//得到年份 temp1=0; while(temp>=28)//超过了一个月 { if(Is_Leap_Year(calendar.w_year)&&temp1==1)//当年是不是闰年/2 月份 270 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { if(temp>=29)temp-=29;//闰年的秒钟数 else break; } else { if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年 else break; } temp1++; } calendar.w_month=temp1+1; //得到月份 calendar.w_date=temp+1; //得到日期 } temp=timecount%86400; //得到秒钟数 calendar.hour=temp/3600; //小时 calendar.min=(temp%3600)/60; //分钟 calendar.sec=(temp%3600)%60; //秒钟 calendar.week=RTC_Get_Week(calendar.w_year,calendar.w_month,calendar.w_date); return 0; } 函数其实就是将存储在秒钟寄存器 RTC->CNTH 和 RTC->CNTL 中的秒钟数据转换为真正 的时间和日期。该代码还用到了一个 calendar 的结构体,calendar 是我们在 rtc.h 里面将要定义 的一个时间结构体,用来存放时钟的年月日时分秒等信息。因为 STM32 的 RTC 只有秒钟计数 器,而年月日,时分秒这些需要我们自己软件计算。我们把计算好的值保存在 calendar 里面, 方便其他程序调用。 最后,我们介绍一下秒钟中断服务函数,该函数代码如下: //RTC 时钟中断 //每秒触发一次 void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_SEC) != RESET)//秒钟中断 { RTC_Get();//更新时间 } if(RTC_GetITStatus(RTC_IT_ALR)!= RESET)//闹钟中断 { RTC_ClearITPendingBit(RTC_IT_ALR); //清闹钟中断 } RTC_ClearITPendingBit(RTC_IT_SEC|RTC_IT_OW); //清闹钟中断 RTC_WaitForLastTask(); } 此部分代码比较简单,我们通过 RTC_GetITStatus 函数来判断发生的是何种中断,如果是 271 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 秒钟中断,则执行一次时间的计算,获得最新时间。从而,我们可以在 calendar 里面读到时间、 日期等信息。 rtc.c 的其他程序,这里就不再介绍了,请大家直接看光盘的源码,接下来我们看看 rtc.h 内 容如下: #ifndef __RTC_H #define __RTC_H //时间结构体 typedef struct { vu8 hour; vu8 min; vu8 sec; //公历日月年周 vu16 w_year; vu8 w_month; vu8 w_date; vu8 week; }_calendar_obj; extern _calendar_obj calendar; //日历结构体 void Disp_Time(u8 x,u8 y,u8 size); void Disp_Week(u8 x,u8 y,u8 size,u8 lang); //在制定位置开始显示时间 //在指定位置显示星期 u8 RTC_Init(void); u8 Is_Leap_Year(u16 year); u8 RTC_Get(void); //初始化 RTC,返回 0,失败;1,成功; //平年,闰年判断 //更新时间 u8 RTC_Get_Week(u16 year,u8 month,u8 day); u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec);//设置时间 #endif 从上面代码可以看到_calendar_obj 结构体所包含的东西,是一个完整的公历信息,包括年、 月、日、周、时、分、秒等 7 个元素。我们以后要知道当前时间,只需要通过 RTC_Get 函数, 执行时钟转换,然后就可以从 calendar 里面读出当前的公历时间了。 最后我们来看看主函数代码: int main(void) { u8 t; delay_init(); //延时函数初始化 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD usmart_dev.init(72); //初始化 USMART POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"RTC TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); 272 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_ShowString(60,110,200,16,16,"2014/3/8"); while(RTC_Init()) //RTC 初始化 ,一定要初始化成功 { LCD_ShowString(60,130,200,16,16,"RTC ERROR! "); delay_ms(800); LCD_ShowString(60,130,200,16,16,"RTC Trying..."); } //显示时间 POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,130,200,16,16," - - "); LCD_ShowString(60,162,200,16,16," : : "); while(1) { if(t!=calendar.sec) { t=calendar.sec; LCD_ShowNum(60,130,calendar.w_year,4,16); LCD_ShowNum(100,130,calendar.w_month,2,16); LCD_ShowNum(124,130,calendar.w_date,2,16); switch(calendar.week) { case 0: LCD_ShowString(60,148,200,16,16,"Sunday "); break; case 1: LCD_ShowString(60,148,200,16,16,"Monday "); break; case 2: LCD_ShowString(60,148,200,16,16,"Tuesday "); break; case 3: LCD_ShowString(60,148,200,16,16,"Wednesday");break; case 4: LCD_ShowString(60,148,200,16,16,"Thursday ");break; case 5: LCD_ShowString(60,148,200,16,16,"Friday ");break; case 6: LCD_ShowString(60,148,200,16,16,"Saturday ");break; } LCD_ShowNum(60,162,calendar.hour,2,16); LCD_ShowNum(84,162,calendar.min,2,16); LCD_ShowNum(108,162,calendar.sec,2,16); LED0=!LED0; } delay_ms(10); }; } 这部分代码就不再需要详细解释了,在包含了 rtc.h 之后,通过判断 calendar.sec 是否改变 来决定要不要更新时间显示。同时我们设置 LED0 每 2 秒钟闪烁一次,用来提示程序已经开始 跑了。 为了方便设置时间,我们在 usmart_config.c 里面,修改 usmart_nametab 如下: struct _m_usmart_nametab usmart_nametab[]= { 273 STM32 不完全手册(库函数版) #if USMART_USE_WRFUNS==1 ALIENTEK MiniSTM32 V3.0 开发板教程 //如果使能了读写操作 (void*)read_addr,"u32 read_addr(u32 addr)", (void*)write_addr,"void write_addr(u32 addr,u32 val)", #endif (void*)delay_ms,"void delay_ms(u16 nms)", (void*)delay_us,"void delay_us(u32 nus)", (void*)RTC_Set,"u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)", }; 将 RTC_Set 加入了 usmart,同时去掉了上一章的一些函数(减少代码量),这样通过串口 就可以直接设置 RTC 时间了。 至此,RTC 实时时钟的软件设计就完成了,接下来就让我们来检验一下,我们的程序是否 正确了。 18.4 下载验证 将程序下载到 MiniSTM32 后,可以看到 DS0 不停的闪烁,提示程序已经在运行了。同时 可以看到 TFTLCD 模块开始显示时间,实际显示效果如图 18.4.1 所示: 图 18.4.1 RTC 实验测试图 如果时间不正确,大家可以用上一章介绍的方法,通过串口调用 RTC_Set 来设置一下当前 时间,如图 18.4.2 所示: 274 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 18.4.2 通过 USMART 设置 RTC 时间 上图中,我们通过 usmart 设置时间为:2014 年 3 月 8 日,22 点 36 分 44 秒,执行完以后, 可以在 LCD 上面看到时间变成了我们所设置的时间。 275 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第十九章 待机唤醒实验 本章我们将向大家介绍 STM32 的待机唤醒功能。在本章中,我们将使用 WK_UP 按键来实 现唤醒和进入待机模式的功能,然后使用 DS0 指示状态。本章将分为如下几个部分: 19.1 STM32 待机模式简介 19.2 硬件设计 19.3 软件设计 19.4 下载验证 276 19.1 STM32 待机模式简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 很多单片机都有低功耗模式,STM32 也不例外。在系统或电源复位以后,微控制器处于运 行状态。运行状态下的 HCLK 为 CPU 提供时钟,内核执行程序代码。当 CPU 不需继续运行时, 可以利用多个低功耗模式来节省功耗,例如等待某个外部事件时。用户需要根据最低电源消耗, 最快速启动时间和可用的唤醒源等条件,选定一个最佳的低功耗模式。STM32 的 3 种低功耗模 式我们在 5.2.4 节有粗略介绍,这里我们再回顾一下。 STM32 的低功耗模式有 3 种: 1)睡眠模式(CM3 内核停止,外设仍然运行) 2)停止模式(所有时钟都停止) 3)待机模式(1.8V 内核电源关闭) 在运行模式下,我们也可以通过降低系统时钟关闭 APB 和 AHB 总线上未被使用的外设的 时钟来降低功耗。三种低功耗模式一览表见表 19.1.1 所示: 表 19.1.1 STM32 低功耗一览表 在这三种低功耗模式中,最低功耗的是待机模式,在此模式下,最低只需要 2uA 左右的电 流。停机模式是次低功耗的,其典型的电流消耗在 20uA 左右。最后就是睡眠模式了。用户可 以根据自己的需求来决定使用哪种低功耗模式。 本章,我们仅对 STM32 的最低功耗模式-待机模式,来做介绍。待机模式可实现 STM32 的最低功耗。该模式是在 CM3 深睡眠模式时关闭电压调节器。整个 1.8V 供电区域被断电。PLL、 HSI 和 HSE 振荡器也被断电。SRAM 和寄存器内容丢失。仅备份的寄存器和待机电路维持供电。 那么我们如何进入待机模式呢?其实很简单,只要按图 19.1.1 所示的步骤执行就可以了: 图 19.1.1 STM32 进入及退出待机模式的条件 图 19.1.1 还列出了退出待机模式的操作,从图 19.1.1 可知,我们有 4 种方式可以退出待机 模式,即当一个外部复位(NRST 引脚)、IWDG 复位、WKUP 引脚上的上升沿或 RTC 闹钟事件 发生时,微控制器从待机模式退出。从待机唤醒后,除了电源控制/状态寄存器(PWR_CSR),所 277 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 有寄存器被复位。 从待机模式唤醒后的代码执行等同于复位后的执行(采样启动模式引脚,读取复位向量等)。 电源控制/状态寄存器(PWR_CSR)将会指示内核由待机状态退出。 在进入待机模式后,除了复位引脚以及被设置为防侵入或校准输出时的 TAMPER 引脚和被 使能的唤醒引脚(WK_UP 脚),其他的 IO 引脚都将处于高阻态。 图 19.1.1 已经清楚的说明了进入待机模式的通用步骤,其中涉及到 2 个寄存器,即电源控 制寄存器(PWR_CR)和电源控制/状态寄存器(PWR_CSR)。下面我们介绍一下这两个寄存器: 电源控制寄存器(PWR_CR),该寄存器的各位描述如图 19.1.2 所示: 图 19.1.2 PWR_CR 寄存器各位描述 这里我们通过设置 PWR_CR 的 PDDS 位,使 CPU 进入深度睡眠时进入待机模式,同时我 278 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 们通过 CWUF 位,清除之前的唤醒位。电源控制/状态寄存器(PWR_CSR)的各位描述如图 19.1.3 所示: 图 19.1.3 PWR_ CSR 寄存器各位描述 这里,我们通过设置 PWR_CSR 的 EWUP 位,来使能 WKUP 引脚用于待机模式唤醒。我 们还可以从 WUF 来检查是否发生了唤醒事件。不过本章我们并没有用到。 通过以上介绍,我们了解了进入待机模式的方法,以及设置 WK_UP 引脚用于把 STM32 从待机模式唤醒的方法。接下来我们讲解通过库函数配置的具体步骤: 1)使能电源时钟。 因为要配置电源控制寄存器,所以必须先使能电源时钟。 在库函数中,使能电源时钟的方法是: RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能 PWR 外设时钟 这个函数非常容易理解。 2) 设置 WK_UP 引脚作为唤醒源。 使能时钟之后后再设置 PWR_CSR 的 EWUP 位,使能 WK_UP 用于将 CPU 从待机模式唤 醒。在库函数中,设置使能 WK_UP 用于唤醒 CPU 待机模式的函数是: PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能 279 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 3)设置 SLEEPDEEP 位,设置 PDDS 位,执行 WFI 指令,进入待机模式。 进入待机模式,首先要设置 SLEEPDEEP 位(该位在系统控制寄存器(SCB_SCR)的第 二位,详见《CM3 权威指南》,第 182 页表 13.1),接着我们通过 PWR_CR 设置 PDDS 位,使 得 CPU 进入深度睡眠时进入待机模式,最后执行 WFI 指令开始进入待机模式,并等待 WK_UP 中断的到来。在库函数中,进行上面三个功能进入待机模式是在函数 PWR_EnterSTANDBYMode 中实现的: void PWR_EnterSTANDBYMode(void); 4)最后编写 WK_UP 中断函数。 因为我们通过 WK_UP 中断(PA0 中断)来唤醒 CPU,所以我们有必要设置一下该中断函 数,同时我们也通过该函数里面进入待机模式。 通过以上几个步骤的设置,我们就可以使用 STM32 的待机模式了,并且可以通过 WK_UP 来唤醒 CPU,我们最终要实现这样一个功能:通过长按(3 秒)WK_UP 按键开机,并让 DS 闪烁,同时 TFTLCD 模块显示一些信息,表示程序已经开始运行,再次长按该键,则进入待机 模式,DS0 关闭,TFTLCD 模块关闭,程序停止运行。类似于手机的开关机。 19.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 按键 3) TFTLCD 模块 本章,我们使用了 WK_UP 按键用于唤醒和进入待机模式。然后通过 DS0 和 TFTLCD 模块 来指示程序是否在运行。这几个硬件的连接前面均有介绍。 19.3 软件设计 打开待机唤醒实验工程,我们可以发现工程中多了一个 wkup.c 和 wkup.h 文件,相关的用 户代码写在这两个文件中。同时,对于待机唤醒功能,我们需要引入 stm32f10x_pwr.c 和 stm32f0x_pwr.h 文件。 打开 wkup.c,可以看到如下关键代码: #include "wkup.h" #include "led.h" #include "delay.h" void Sys_Standby(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //使能 PWR 外设时钟 PWR_WakeUpPinCmd(ENABLE); //使能唤醒管脚功能 PWR_EnterSTANDBYMode(); //进入待命(STANDBY)模式 } //系统进入待机模式 void Sys_Enter_Standby(void) { RCC_APB2PeriphResetCmd(0X01FC,DISABLE); //复位所有 IO 口 Sys_Standby(); } 280 STM32 不完全手册(库函数版) //检测 WKUP 脚的信号 //返回值 1:连续按下 3s 以上 // 0:错误的触发 u8 Check_WKUP(void) ALIENTEK MiniSTM32 V3.0 开发板教程 { u8 t=0; //记录按下的时间 LED0=0; //亮灯 DS0 while(1) { if(WKUP_KD) { t++; //已经按下了 delay_ms(30); if(t>=100) //按下超过 3 秒钟 { LED0=0; //点亮 DS0 return 1; //按下 3s 以上了 } }else { LED0=1; return 0; //按下不足 3 秒 } } } //中断,检测到 PA0 脚的一个上升沿. //中断线 0 线上的中断检测 void EXTI0_IRQHandler(void) { EXTI_ClearITPendingBit(EXTI_Line0); // 清除 LINE10 上的中断标志位 if(Check_WKUP())//关机? { Sys_Enter_Standby(); } } //PA0 WKUP 唤醒初始化 void WKUP_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; 281 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);//使能 GPIOA 和复用功能时钟 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_0; //PA.0 GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPD;//上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 IO //使用外部中断方式 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); //中断线 0 连接 GPIOA.0 EXTI_InitStructure.EXTI_Line = EXTI_Line0; //设置按键所有的外部线路 EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //设外外部中断模式 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); // 初始化外部中断 NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键所在的外部中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //从优先级 2 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //初始化外设 NVIC 寄存器 if(Check_WKUP()==0) Sys_Standby(); //不是开机,进入待机模式 } 该部分代码比较简单,我们在这里说明一下,在 void WKUP_Init(void)函数里面,我们要 先判断 WK_UP 是否按下了 3 秒钟,来决定要不要开机,如果没有按下 3 秒钟,程序直接就进 入了待机模式。所以在下载完代码的时候,是看不到任何反应的。我们必须先按 WK_UP 按键 3 秒开机,才能看到 DS0 闪烁。 讲解完 wkup.c 文件,接下来我们看看头文件 wkup.h: #ifndef __WKUP_H #define __WKUP_H #include "sys.h" #define WKUP_KD PAin(0) u8 Check_WKUP(void); void WKUP_Init(void); void Sys_Enter_Standby(void); //PA0 检测是否外部 WK_UP 按键按下 //检测 WKUP 脚的信号 //PA0 WKUP 唤醒初始化 //系统进入待机模式 #endif 该部分代码,也很简单,我们就不多说了。最后我们看看主函数代码: int main(void) { delay_init(); //延时函数初始化 NVIC_Configuration(); 282 STM32 不完全手册(库函数版) uart_init(9600); LED_Init(); WKUP_Init(); LCD_Init(); ALIENTEK MiniSTM32 V3.0 开发板教程 //串口初始化为 9600 //初始化与 LED 连接的硬件接口 //初始化 WK_UP 按键,同时检测是否正常开机? //初始化 LCD POINT_COLOR=RED; LCD_ShowString(30,50,200,16,16,"Mini STM32"); LCD_ShowString(30,70,200,16,16,"WKUP TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2014/3/8"); while(1) { LED0=!LED0; delay_ms(250); } } 这里我们先初始化 LED 和 WK_UP 按键(通过 WKUP_Init()函数初始化),如果检测到 有长按 WK_UP 按键 3 秒以上,则开机,并执行 LCD 初始化,在 LCD 上面显示一些内容,如 果没有长按,则在 WKUP_Init 里面,调用 Sys_Enter_Standby 函数,直接进入待机模式了。 开机后,在死循环里面等待 WK_UP 中断的到来,在得到中断后,在中断函数里面判断 WK_UP 按下的时间长短,来决定是否进入待机模式,如果按下时间超过 3 秒,则进入待机, 否则退出中断,继续执行 main 函数的死循环等待,同时不停的取反 LED0,让红灯闪烁。 代码部分就介绍到这里,大家记住下载代码后,一定要长按 WK_UP 按键,来开机,否则 将直接进入待机模式,无任何现象。 19.4 下载与测试 在代码编译成功之后,下载代码到 ALIENTEK MiniSTM32 开发板上,此时,看到开发板 DS0 亮了一下(Check_WKUP 函数执行了 LED0=0 的操作),就没有反应了。其实这是正常的, 在程序下载完之后,开发板检测不到 WK_UP 的持续按下(3 秒以上),所以直接进入待机模式, 看起来和没有下载代码一样。此时,我们长按 WK_UP 按键 3 秒钟左右,可以看到 DS0 开始闪 烁。然后再长按 WK_UP,DS0 会灭掉,程序再次进入待机模式。 283 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二十章 ADC 实验 本章我们将向大家介绍 STM32 的 ADC 功能。在本章中,我们将使用 STM32 的 ADC1 通 道 1 来采样外部电压值,并在 TFTLCD 模块上显示出来。本章将分为如下几个部分: 20.1 STM32 ADC 简介 20.2 硬件设计 20.3 软件设计 20.4 下载验证 284 20.1 STM32 ADC 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 STM32 拥有 1~3 个 ADC(STM32F101/102 系列只有 1 个 ADC),这些 ADC 可以独立使用, 也可以使用双重模式(提高采样率)。STM32 的 ADC 是 12 位逐次逼近型的模拟数字转换器。 它有 18 个通道,可测量 16 个外部和 2 个内部信号源。各通道的 A/D 转换可以单次、连续、扫 描或间断模式执行。ADC 的结果可以左对齐或右对齐方式存储在 16 位数据寄存器中。 模拟看 门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阀值。 STM32F103 系列最少都拥有 2 个 ADC,我们选择的 STM32F103RCT 包含有 3 个 ADC。 STM32 的 ADC 最大的转换速率为 1Mhz,也就是转换时间为 1us(在 ADCCLK=14M,采样周期 为 1.5 个 ADC 时钟下得到),不要让 ADC 的时钟超过 14M,否则将导致结果准确度下降。 STM32 将 ADC 的转换分为 2 个通道组:规则通道组和注入通道组。规则通道相当于你正 常运行的程序,而注入通道呢,就相当于中断。在你程序正常执行的时候,中断是可以打断你 的执行的。同这个类似,注入通道的转换可以打断规则通道的转换, 在注入通道被转换完成之 后,规则通道才得以继续转换。 通过一个形象的例子可以说明:假如你在家里的院子内放了 5 个温度探头,室内放了 3 个 温度探头;你需要时刻监视室外温度即可,但偶尔你想看看室内的温度;因此你可以使用规则 通道组循环扫描室外的 5 个探头并显示 AD 转换结果,当你想看室内温度时,通过一个按钮启 动注入转换组(3 个室内探头)并暂时显示室内温度,当你放开这个按钮后,系统又会回到规则通 道组继续检测室外温度。从系统设计上,测量并显示室内温度的过程中断了测量并显示室外温 度的过程,但程序设计上可以在初始化阶段分别设置好不同的转换组,系统运行中不必再变更 循环转换的配置,从而达到两个任务互不干扰和快速切换的结果。可以设想一下,如果没有规 则组和注入组的划分,当你按下按钮后,需要从新配置 AD 循环扫描的通道,然后在释放按钮 后需再次配置 AD 循环扫描的通道。 上面的例子因为速度较慢,不能完全体现这样区分(规则通道组和注入通道组)的好处,但 在工业应用领域中有很多检测和监视探头需要较快地处理,这样对 AD 转换的分组将简化事件 处理的程序并提高事件处理的速度。 STM32 其 ADC 的规则通道组最多包含 16 个转换,而注入通道组最多包含 4 个通道。关于 这两个通道组的详细介绍,请参考《STM32 参考手册的》第 155 页,第 11 章。 STM32 的 ADC 可以进行很多种不同的转换模式,这些模式在《STM32 参考手册》的第 11 章也都有详细介绍,我们这里就不在一一列举了。我们本章仅介绍如何使用规则通道的单次转 换模式。 STM32 的 ADC 在单次转换模式下,只执行一次转换,该模式可以通过 ADC_CR2 寄存器 的 ADON 位(只适用于规则通道)启动,也可以通过外部触发启动(适用于规则通道和注入通 道),这是 CONT 位为 0。 以规则通道为例,一旦所选择的通道转换完成,转换结果将被存在 ADC_DR 寄存器中, EOC(转换结束)标志将被置位,如果设置了 EOCIE,则会产生中断。然后 ADC 将停止,直 到下次启动。 接下来,我们介绍一下我们执行规则通道的单次转换,需要用到的 ADC 寄存器。第一个 要介绍的是 ADC 控制寄存器(ADC_CR1 和 ADC_CR2)。ADC_CR1 的各位描述如图 22.1.1 所 示: 285 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 20.1.1 ADC_CR1 寄存器各位描述 这里我们不再详细介绍每个位,而是抽出几个我们本章要用到的位进行针对性的介绍,详 细的说明及介绍,请参考《STM32 参考手册》第 11 章的相关章节。 ADC_CR1 的 SCAN 位,该位用于设置扫描模式,由软件设置和清除,如果设置为 1,则 使用扫描模式,如果为 0,则关闭扫描模式。在扫描模式下,由 ADC_SQRx 或 ADC_JSQRx 寄 存器选中的通道被转换。如果设置了 EOCIE 或 JEOCIE,只在最后一个通道转换完毕后才会产 生 EOC 或 JEOC 中断。 ADC_CR1[19:16]用于设置 ADC 的操作模式,详细的对应关系如图 20.1.2 所示: 图 20.1.2 ADC 操作模式 本章我们要使用的是独立模式,所以设置这几位为 0 就可以了。接着我们介绍 ADC_CR2, 该寄存器的各位描述如图 20.1.3 所示: 图 20.1.3 ADC_CR2 寄存器操作模式 该寄存器我们也只针对性的介绍一些位:ADON 位用于开关 AD 转换器。而 CONT 位用于 设置是否进行连续转换,我们使用单次转换,所以 CONT 位必须为 0。CAL 和 RSTCAL 用于 AD 校准。ALIGN 用于设置数据对齐,我们使用右对齐,该位设置为 0。 EXTSEL[2:0]用于选择启动规则转换组转换的外部事件,详细的设置关系如图 20.1.4 所示: 286 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 20.1.4 ADC 选择启动规则转换事件设置 我们这里使用的是软件触发(SWSTART),所以设置这 3 个位为 111。ADC_CR2 的 SWSTART 位用于开始规则通道的转换,我们每次转换(单次转换模式下)都需要向该位写 1。 AWDEN 为用于使能温度传感器和 Vrefint。STM32 内部的温度传感器我们将在下一节介绍。 第二个要介绍的是 ADC 采样事件寄存器(ADC_SMPR1 和 ADC_SMPR2),这两个寄存器 用于设置通道 0~17 的采样时间,每个通道占用 3 个位。ADC_SMPR1 的各位描述如图 20.1.5 所示: 图 20.1.5 ADC_SMPR1 寄存器各位描述 ADC_SMPR2 的各位描述如下图 20.1.6 所示: 287 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 20.1.6 ADC_SMPR2 寄存器各位描述 对于每个要转换的通道,采样时间建议尽量长一点,以获得较高的准确度,但是这样会降 低 ADC 的转换速率。ADC 的转换时间可以由以下公式计算: Tcovn=采样时间+12.5 个周期 其中:Tcovn 为总转换时间,采样时间是根据每个通道的 SMP 位的设置来决定的。例如, 当 ADCCLK=14Mhz 的时候,并设置 1.5 个周期的采样时间,则得到:Tcovn=1.5+12.5=14 个周 期=1us。 第三个要介绍的是 ADC 规则序列寄存器(ADC_SQR1~3),该寄存器总共有 3 个,这几个 寄存器的功能都差不多,这里我们仅介绍一下 ADC_SQR1,该寄存器的各位描述如图 20.1.7 所 示: 图 20.1.7 ADC_ SQR1 寄存器各位描述 L[3:0]用于存储规则序列的长度,我们这里只用了 1 个,所以设置这几个位的值为 0。其 他的 SQ13~16 则存储了规则序列中第 13~16 通道的编号(编号范围:0~17)。另外两个规则序 288 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 列寄存器同 ADC_SQR1 大同小异,我们这里就不再介绍了,要说明一点的是:我们选择的是 单次转换,所以只有一个通道在规则序列里面,这个序列就是 SQ1,通过 ADC_SQR3 的最低 5 位(也就是 SQ1)设置。 第四个要介绍的是 ADC 规则数据寄存器(ADC_DR)。规则序列中的 AD 转化结果都将被存 在这个寄存器里面,而注入通道的转换结果被保存在 ADC_JDRx 里面。ADC_DR 的各位描述 如图 20.1.8: 图 20.1.8 ADC_ JDRx 寄存器各位描述 这里要提醒一点的是,该寄存器的数据可以通过 ADC_CR2 的 ALIGN 位设置左对齐还是 右对齐。在读取数据的时候要注意。 最后一个要介绍的 ADC 寄存器为 ADC 状态寄存器(ADC_SR),该寄存器保存了 ADC 转 换时的各种状态。该寄存器的各位描述如图 20.1.9 所示: 289 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 20.1.9 ADC_ SR 寄存器各位描述 这里我们要用到的是 EOC 位,我们通过判断该位来决定是否此次规则通道的 AD 转换已经 完成,如果完成我们就从 ADC_DR 中读取转换结果,否则等待转换完成。 通过以上寄存器的介绍,我们了解了 STM32 的单次转换模式下的相关设置,下面我们介 绍使用库函数的函数来设定使用 ADC1 的通道 1 进行 AD 转换。这里需要说明一下,使用到的 库函数分布在 stm32f10x_adc.c 文件和 stm32f10x_adc.h 文件中。下面讲解其详细设置步骤: 1)开启 PA 口和 ADC1 时钟,设置 PA1 为模拟输入。 STM32F103RCT6 的 ADC 通道 1 在 PA1 上,所以,我们先要使能 PORTA 的时钟,然后设 置 PA1 为模拟输入。使能 GPIOA 和 ADC 时钟用 RCC_APB2PeriphClockCmd 函数,设置 PA1 的输入方式,使用 GPIO_Init 函数即可。这里我们列出 STM32 的 ADC 通道与 GPIO 对应表: 290 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 2)复位 ADC1,同时设置 ADC1 分频因子。 开启 ADC1 时钟之后,我们要复位 ADC1,将 ADC1 的全部寄存器重设为缺省值之后我们 就可以通过 RCC_CFGR 设置 ADC1 的分频因子。分频因子要确保 ADC1 的时钟(ADCCLK) 不要超过 14Mhz。 这个我们设置分频因子位 6,时钟为 72/6=12MHz,库函数的实现方法是: RCC_ADCCLKConfig(RCC_PCLK2_Div6); ADC 时钟复位的方法是: ADC_DeInit(ADC1); 这个函数非常容易理解,就是复位指定的 ADC。 3)初始化 ADC1 参数,设置 ADC1 的工作模式以及规则序列的相关信息。 在设置完分频因子之后,我们就可以开始 ADC1 的模式配置了,设置单次转换模式、触发 方式选择、数据对齐方式等都在这一步实现。同时,我们还要设置 ADC1 规则序列的相关信息, 我们这里只有一个通道,并且是单次转换的,所以设置规则序列中通道数为 1。这些在库函数 中是通过函数 ADC_Init 实现的,下面我们看看其定义: void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct); 从函数定义可以看出,第一个参数是指定 ADC 号。这里我们来看看第二个参数,跟其他外设 初始化一样,同样是通过设置结构体成员变量的值来设定参数。 typedef struct { uint32_t ADC_Mode; FunctionalState ADC_ScanConvMode; FunctionalState ADC_ContinuousConvMode; uint32_t ADC_ExternalTrigConv; uint32_t ADC_DataAlign; uint8_t ADC_NbrOfChannel; }ADC_InitTypeDef; 参数 ADC_Mode 故名是以是用来设置 ADC 的模式。前面讲解过,ADC 的模式非常多,包括独 291 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 立模式,注入同步模式等等,这里我们选择独立模式,所以参数为 ADC_Mode_Independent。 参数 ADC_ScanConvMode 用来设置是否开启扫描模式,因为我们的实验是单通道单次转换, 所以这里我们选择不开启值 DISABLE 即可。 参数 ADC_ContinuousConvMode 用来设置是否开启连续转换模式,因为是单次转换模式,所以 我们选择不开启连续转换模式,DISABLE 即可。 参数 ADC_ExternalTrigConv 是用来设置启动规则转换组转换的外部事件,这里我们选择软件触 发,选择值为 ADC_ExternalTrigConv_None 即可。 参数 DataAlign 用来设置 ADC 数据对齐方式是左对齐还是右对齐,这里我们选择右对齐方式 ADC_DataAlign_Right。 参数 ADC_NbrOfChannel 用来设置规则序列的长度,我们实验只开启一个通道,所以值为 1 即 可。 通过上面对每个参数的讲解,下面来看看我们的初始化范例: ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC 工作模式:独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //AD 单通道模式 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //AD 单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的 ADC 通道的数目 1 ADC_Init(ADC1, &ADC_InitStructure); //根据指定的参数初始化外设 ADCx 5)使能 ADC 并校准。 在设置完了以上信息后,我们就使能 AD 转换器,执行复位校准和 AD 校准,注意这两步 是必须的!不校准将导致结果很不准确。 使能指定的 ADC 的方法是: ADC_Cmd(ADC1, ENABLE); //使能指定的 ADC1 执行复位校准的方法是: ADC_ResetCalibration(ADC1); 执行 ADC 校准的方法是: ADC_StartCalibration(ADC1); //开始指定 ADC1 的校准状态 记住,每次进行校准之后要等待校准结束。这里是通过获取校准状态来判断是否校准是否结束。 下面我们一一列出复位校准和 AD 校准的等待结束方法: while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束 while(ADC_GetCalibrationStatus(ADC1)); //等待校 AD 准结束 6)读取 ADC 值。 在上面的校准完成之后,ADC 就算准备好了。接下来我们要做的就是设置规则序列 1 里面 的通道,采样顺序,以及通道的采样周期,然后启动 ADC 转换。在转换结束后,读取 ADC 转 换结果值就是了。这里设置规则序列通道以及采样周期的函数是: void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime); 我们这里是规则序列中的第 1 个转换,同时采样周期为 239.5,所以设置为: ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); 软件开启 ADC 转换的方法是: 292 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 ADC_SoftwareStartConvCmd(ADC1, ENABLE);//使能指定的 ADC1 的软件转换启动功能 开启转换之后,就可以获取转换 ADC 转换结果数据,方法是: ADC_GetConversionValue(ADC1); 同时在 AD 转换中,我们还要根据状态寄存器的标志位来获取 AD 转换的各个状态信息。库函 数获取 AD 转换的状态信息的函数是: FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG) 比如我们要判断 ADC1d 的转换是否结束,方法是: while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束 通过以上几个步骤的设置,我们就能正常的使用 STM32 的 ADC1 来执行 AD 转换操作了。 这里还需要说明一下 ADC 的参考电压,MiniSTM32 开发板使用的是 STM32F103RCT6, 该芯片没有外部参考电压引脚,ADC 的参考电压直接取自 VDDA,也就是 3.3V。 20.2 硬件设计 本实验用到的硬件资源有: 1) 指示灯 DS0 2) TFTLCD 模块 3) ADC 4) 杜邦线 前面两个均已介绍过,而 ADC 属于 STM32 内部资源,实际上我们只需要软件设置就可以 正常工作,不过我们需要在外部连接其端口到被测电压上面。本章,我们通过 ADC1 的通道 1 (PA1)来读取外部电压值,MiniSTM32 开发板没有设计参考电压源在上面,但是板上有几个 可以提供测试的地方:1,3.3V 电源。2,GND。3,后备电池。注意:这里不能接到板上 5V 电源上去测试,这可能会烧坏 ADC!。 因为要连接到其他地方测试电压,所以我们需要一根杜邦线,或者自备的连接线也可以, 一头插在 PA1 排针上(在 P3 上),另外一头就接你要测试的电压点(确保该电压不大于 3.3V 即可)。如果是测量外部电压,则还需要和开发板共地,开发板上有很多 GND 的排针,随便连 接一个共地即可。 20.3 软件设计 打开我们的 ADC 转换实验,可以看到工程中多了一个 adc.c 文件和 adc.h 文件。同时 ADC 相关的库函数是在 stm32f10x_adc.c 文件和 stm32f10x_adc.h 文件中。 打开 adc.c,可以看到代码如下: //初始化 ADC //这里我们仅以规则通道为例 //我们默认将开启通道 0~3 void Adc_Init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1 , ENABLE ); //使能 ADC1 通道时钟 RCC_ADCCLKConfig(RCC_PCLK2_Div6); //设置 ADC 分频因子 6 //72M/6=12,ADC 最大时间不能超过 14M 293 STM32 不完全手册(库函数版) //PA1 作为模拟通道输入引脚 ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_InitStructure.GPIO_Pin =GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入 GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化 GPIOA.1 ADC_DeInit(ADC1); //复位 ADC1,将外设 ADC1 的全部寄存器重设为缺省值 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC 独立模式 ADC_InitStructure.ADC_ScanConvMode = DISABLE; //单通道模式 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //单次转换模式 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//转换由 //软件而不是外部触发启动 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC 数据右对齐 ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的 ADC 通道的数目 ADC_Init(ADC1, &ADC_InitStructure); //根据指定的参数初始化外设 ADCx ADC_Cmd(ADC1, ENABLE); //使能指定的 ADC1 ADC_ResetCalibration(ADC1); //开启复位校准 while(ADC_GetResetCalibrationStatus(ADC1)); //等待复位校准结束 ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); //开启 AD 校准 //等待校准结束 } //获得 ADC 值 //ch:通道值 0~3 u16 Get_Adc(u8 ch) { //设置指定 ADC 的规则组通道,设置它们的转化顺序和采样时间 ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //通道 1 //规则采样顺序值为 1,采样时间为 239.5 周期 ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的 ADC1 的软件转换功能 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束 return ADC_GetConversionValue(ADC1); //返回最近一次 ADC1 规则组的转换结果 } u16 Get_Adc_Average(u8 ch,u8 times) { u32 temp_val=0; u8 t; for(t=0;t200)dacval-=200; else dacval=0; DAC_SetChannel1Data(DAC_Align_12b_R, dacval);//??DAC? } if(t==10||key==KEY0_PRES||key==WKUP_PRES) //WKUP/KEY1 按下了,或者定时时间到了 { adcx=DAC_GetDataOutputValue(DAC_Channel_1);//??????DAC?? LCD_ShowxNum(124,150,adcx,4,16,0); //显示 DAC 寄存器值 temp=(float)adcx*(3.3/4096); //得到 DAC 电压值 adcx=temp; LCD_ShowxNum(124,170,temp,1,16,0); //显示电压值整数部分 temp-=adcx; temp*=1000; LCD_ShowxNum(140,170,temp,3,16,0X80); //显示电压值的小数部分 adcx=Get_Adc_Average(ADC_Channel_1,10); // 得 到 ADC 转 换 值 temp=(float)adcx*(3.3/4096); //得到 ADC 电压值 adcx=temp; LCD_ShowxNum(124,190,temp,1,16,0); //显示电压值整数部分 temp-=adcx; temp*=1000; LCD_ShowxNum(140,190,temp,3,16,0X80); //显示电压值的小数部分 LED0=!LED0; t=0; } delay_ms(10); } } 此部分代码,我们先对需要用到的模块进行初始化,然后显示一些提示信息,本章我们 通过 WK_UP 和 KEY0 来实现对 DAC 输出的幅值控制。按下 WK_UP 增加,按 KEY0 减小。 310 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 同时在 LCD 上面显示 DHR12R1 寄存器的值、DAC 设计输出电压以及 ADC 采集到的 DAC 输出电压。 22.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看 到 LCD 显示如图 22.4.1 所示: 图 22.4.1 DAC 实验测试图 同时伴随 DS0 的不停闪烁,提示程序在运行。此时,我们通过按 WK_UP 按键,可以 看到输出电压增大,按 KEY0 则变小。 311 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二十三章 DMA 实验 本章我们将向大家介绍 STM32 的 DMA。在本章中,我们将利用 STM32 的 DMA 来实 现串口数据传送,并在 TFTLCD 模块上显示当前的传送进度。本章分为如下几个部分: 23.1 STM32 DMA 简介 23.2 硬件设计 23.3 软件设计 23.4 下载验证 312 23.1 STM32 DMA 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接 控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备 开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。 STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中),DMA1 有 7 个通道。DMA2 有 5 个通道。每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁起 来协调各个 DMA 请求的优先权。 STM32 的 DMA 有以下一些特性: ●每个通道都直接连接专用的硬件 DMA 请求,每个通道都同样支持软件触发。这些功能 通过软件来配置。 ●在七个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),假如 在相等优先权时由硬件决定(请求 0 优先于请求 1,依此类推) 。 ●独立的源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和 目标地址必须按数据传输宽度对齐。 ●支持循环的缓冲器管理 ●每个通道都有 3 个事件标志(DMA 半传输,DMA 传输完成和 DMA 传输出错),这 3 个 事件标志逻辑或成为一个单独的中断请求。 ●存储器和存储器间的传输 ●外设和存储器,存储器和外设的传输 ●闪存、SRAM、外设的 SRAM、APB1 APB2 和 AHB 外设均可作为访问的源和目标。 ●可编程的数据传输数目:最大为 65536 STM32F103RCT6 有两个 DMA 控制器,DMA1 和 DMA2,本章,我们仅针对 DMA1 进行 介绍。 从外设(TIMx、ADC、SPIx、I2Cx 和 USARTx)产生的 DMA 请求,通过逻辑或输入到 DMA 控制器,这就意味着同时只能有一个请求有效。外设的 DMA 请求,可以通过设置相应的 外设寄存器中的控制位,被独立地开启或关闭。 表 23.1.1 是 DMA1 各通道一览表: 表 23.1.1 DMA1 各通道一览表 这里解释一下上面说的逻辑或,例如通道 1 的几个 DMA1 请求(ADC1、TIM2_CH3、TIM4_CH1), 这几个是通过逻辑或到通道 1 的,这样我们在同一时间,就只能使用其中的一个。其他通道也 是类似的。 这里我们要使用的是串口 1 的 DMA 传送,也就是要用到通道 4。接下来,我们介绍一下 DMA 313 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 设置相关的几个寄存器。 第一个是 DMA 中断状态寄存器(DMA_ISR)。该寄存器的各位描述如图 23.1.1 所示: 图 23.1.1 DMA_ISR 寄存器各位描述 我们如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去,即使 没开启,我们也可以通过查询这些位来获得当前 DMA 传输的状态。这里我们常用的是 TCIFx, 即通道 DMA 传输完成与否的标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只 能通过其他的操作来清除。 第二个是 DMA 中断标志清除寄存器(DMA_IFCR)。该寄存器的各位描述如图 23.1.2 所示: 314 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 23.1.2 DMA_IFCR 寄存器各位描述 DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后, 我们必须通过向该位寄存器对应的位写入 0 来清除。 第三个是 DMA 通道 x 配置寄存器(DMA_CCRx)(x=1~7,下同)。该寄存器的我们在这里就不 贴出来了,见《STM32 参考手册》第 150 页 10.4.3 一节。该寄存器控制着 DMA 的很多相关信息, 包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、使能等 都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器。 第四个是 DMA 通道 x 传输数据量寄存器(DMA_CNDTRx)。这个寄存器控制 DMA 通道 x 的每次 传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少, 当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存 器的值来知道当前 DMA 传输的进度。 第五个是 DMA 通道 x 的外设地址寄存器(DMA_CPARx)。该寄存器用来存储 STM32 外设的地 址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使 用其他外设,就修改成相应外设的地址就行了。 最后一个是 DMA 通道 x 的存储器地址寄存器(DMA_CMARx),该寄存器和 DMA_CPARx 差不多, 但是是用来放存储器的地址的。比如我们使用 SendBuf[5200]数组来做存储器,那么我们在 DMA_CMARx 中写入&SendBuff 就可以了。 DMA 相关寄存器就为大家介绍到这里,此节我们要用到串口 1 的发送,属于 DMA1 的通道 4, 接下来我们就介绍库函数下 DMA1 通道 4 的配置步骤: 1)使能 DMA 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟 2)初始化 DMA 通道 4 参数 315 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 前面讲解过,DMA 通道配置参数种类比较繁多,包括内存地址,外设地址,传输数据长度, 数据宽度,通道优先级等等。这些参数的配置在库函数中都是在函数 DMA_Init 中完成,下面我 们看看函数定义: void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct) 函数的第一个参数是指定初始化的 DMA 通道号,这个很容易理解,下面我们主要看看第二 个参数。跟其他外设一样,同样是通过初始化结构体成员变量值来达到初始化的目的,下面我 们来看看 DMA_InitTypeDef 结构体的定义: typedef struct { uint32_t DMA_PeripheralBaseAddr; uint32_t DMA_MemoryBaseAddr; uint32_t DMA_DIR; uint32_t DMA_BufferSize; uint32_t DMA_PeripheralInc; uint32_t DMA_MemoryInc; uint32_t DMA_PeripheralDataSize; uint32_t DMA_MemoryDataSize; uint32_t DMA_Mode; uint32_t DMA_Priority; uint32_t DMA_M2M; }DMA_InitTypeDef; 这个结构体的成员比较多,但是每个成员变量的意义我们在前面基本都已经讲解过,这里 我们一一做个简要的介绍。 第一个参数 DMA_PeripheralBaseAddr 用来设置 DMA 传输的外设基地址,比如要进行串口 DMA 传输,那么外设基地址为串口接受发送数据存储器 USART1->DR 的地址,表示方法为 &USART1->DR。 第二个参数 DMA_MemoryBaseAddr 为内存基地址,也就是我们存放 DMA 传输数据的内存地址。 第三个参数 DMA_DIR 设置数据传输方向,决定是从外设读取数据到内存还送从内存读取数 据发送到外设,也就是外设是源地还是目的地,这里我们设置为从内存读取数据发送到串口, 所以外设自然就是目的地了,所以选择值为 DMA_DIR_PeripheralDST。 第四个参数 DMA_BufferSize 设置一次传输数据量的大小,这个很容易理解。 第五个参数 DMA_PeripheralInc 设置传输数据的时候外设地址是不变还是递增。如果设置 为递增,那么下一次传输的时候地址加 1,这里因为我们是一直往固定外设地址&USART1->DR 发送数据,所以地址不递增,值为 DMA_PeripheralInc_Disable; 第 六 个 参 数 DMA_MemoryInc 设 置 传 输 数 据 时 候 内 存 地 址 是 否 递 增 。 这 个 参 数 和 DMA_PeripheralInc 意思接近,只不过针对的是内存。这里我们的场景是将内存中连续存储单 元的数据发送到串口,毫无疑问内存地址是需要递增的,所以值为 DMA_MemoryInc_Enable。 第七个参数 DMA_PeripheralDataSize 用来设置外设的的数据长度是为字节传输(8bits), 半 字 传 输 (16bits) 还 是 字 传 输 (32bits) , 这 里 我 们 是 8 位 字 节 传 输 , 所 以 值 设 置 为 DMA_PeripheralDataSize_Byte。 第八个参数 DMA_MemoryDataSize 是用来设置内存的数据长度,和第七个参数意思接近,这 里我们同样设置为字节传输 DMA_MemoryDataSize_Byte。 316 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第九个参数 DMA_Mode 用来设置 DMA 模式是否循环采集,也就是说,比如我们要从内存中采 集 64 个字节发送到串口,如果设置为重复采集,那么它会在 64 个字节采集完成之后继续从内 存的第一个地址采集,如此循环。这里我们设置为一次连续采集完成之后不循环。所以设置值 为 DMA_Mode_Normal。在我们下面的实验中,如果设置此参数为循环采集,那么你会看到串口 不停的打印数据,不会中断,大家在实验中可以修改这个参数测试一下。 第十个参数是设置 DMA 通道的优先级,有低,中,高,超高三种模式,这个在前面讲解过, 这里我们设置优先级别为中级,所以值为 DMA_Priority_Medium。如果要开启多个通道,那么 这个值就非常有意义。 第 十 一 个 参 数 DMA_M2M 设 置 是 否 是 存 储 器 到 存 储 器 模 式 传 输 , 这 里 我 们 选 择 DMA_M2M_Disable。 这里我们给出上面场景的实例代码: DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = &USART1->DR; //DMA 外设 ADC 基地址 DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //从内存读取发送到外设 DMA_InitStructure.DMA_BufferSize = 64; //DMA 通道的 DMA 缓存的大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设地址不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //8 位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 8 位 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA 通道 x 拥有中优先级 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输 DMA_Init(DMA_CHx, &DMA_InitStructure); //根据指定的参数初始化 3)使能串口 DMA 发送 进行 DMA 配置之后,我们就要开启串口的 DMA 发送功能,使用的函数是: USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); 如果是要使能串口 DMA 接受,那么第二个参数修改为 USART_DMAReq_Rx 即可。 4)使能 DMA1 通道 4,启动传输。 使能串口 DMA 发送之后,我们接着就要使能 DMA 传输通道: DMA_Cmd(DMA_CHx, ENABLE); 通过以上 3 步设置,我们就可以启动一次 USART1 的 DMA 传输了。 5)查询 DMA 传输状态 在 DMA 传输过程中,我们要查询 DMA 传输通道的状态,使用的函数是: FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG) 比如我们要查询 DMA 通道 4 传输是否完成,方法是: DMA_GetFlagStatus(DMA2_FLAG_TC4); 这里还有一个比较重要的函数就是获取当前剩余数据量大小的函数: uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx) 比如我们要获取 DMA 通道 4 还有多少个数据没有传输,方法是: DMA_GetCurrDataCounter(DMA1_Channel4); DMA 相关的库函数我们就讲解到这里,大家可以查看固件库中文手册详细了解。 317 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 通过以上 5 步设置,我们就可以启动一次 USART1 的 DMA 传输了。 23.2 硬件设计 所以本章用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) DMA 本章我们将利用外部按键 KEY0 来控制 DMA 的传送,每按一次 KEY0,DMA 就传送一次数据到 USART1,然后在 TFTLCD 模块上显示进度等信息。DS0 还是用来做为程序运行的指示灯。 本章实验需要注意 P4 口的 RXD 和 TXD 是否和旁边的 PA9 和 PA10 连接上了,如果没有,请 先连接。 23.3 软件设计 打开我们的 DMA 传输实验,可以发现,我们的实验中多了 dma.c 文件和其头文件 dma.h, 同时我们要引入 dma 相关的库函数文件 stm32f10x_dma.c 和 stm32f10x_dma.h。 打开 dma.c 文件,代码如下: #include "dma.h" DMA_InitTypeDef DMA_InitStructure; u16 DMA1_MEM_LEN;//保存 DMA 每次数据传送的长度 //DMA1 的各通道配置 //这里的传输形式是固定的,这点要根据不同的情况来修改 //从存储器->外设模式/8 位数据宽度/存储器增量模式 //DMA_CHx:DMA 通道 CHx //cpar:外设地址 //cmar:存储器地址 //cndtr:数据传输量 void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr) { RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能 DMA 时钟 DMA_DeInit(DMA_CHx); //将 DMA 的通道 1 寄存器重设为缺省值 DMA1_MEM_LEN=cndtr; DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA 外设 ADC 基地址 DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA 内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向内存到外设 DMA_InitStructure.DMA_BufferSize = cndtr; //DMA 通道的 DMA 缓存的大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//内存地址寄存器递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为 8 位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度 //为 8 位 318 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DM 通道拥有中优先级 DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //非内存到内存传输 DMA_Init(DMA_CHx, &DMA_InitStructure); //初始化 DMA 的通道 } //开启一次 DMA 传输 void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx) { DMA_Cmd(DMA_CHx, DISABLE ); //关闭 USART1 TX DMA1 所指示的通道 DMA_SetCurrDataCounter(DMA1_Channel4,DMA1_MEM_LEN);//设置 DMA 缓存的大小 DMA_Cmd(DMA_CHx, ENABLE); //使能 USART1 TX DMA1 所指示的通道 } 该部分代码仅仅 2 个函数,MYDMA_Config 函数,基本上就是按照我们上面介绍的步骤来初 始化 DMA 的,该函数在外部只能修改通道、源地址、目标地址和传输数据量等几个参数,更多 的其他设置只能在该函数内部修改。 MYDMA_Enable 函数用来产生一次 DMA 传输,该函数每执行一次,DMA 就发送一次。对照前 面的配置步骤的详细讲解看看这部分代码即可。 最后我们看看 main 函数如下: const u8 TEXT_TO_SEND[]={"ALIENTEK Mini STM32 DMA 串口实验"}; #define TEXT_LENTH sizeof(TEXT_TO_SEND)-1 //TEXT_TO_SEND 字符串长度(不包含结束符) u8 SendBuff[(TEXT_LENTH+2)*100]; int main(void) { u16 i; u8 t=0; float pro=0; //进度 delay_init(); //延时函数初始化 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 MYDMA_Config(DMA1_Channel4,(u32)&USART1->DR,(u32)SendBuff,(TEXT_LENTH+2)*100); //DMA1 通道 4,外设为串口 1,存储器为 SendBuff,长(TEXT_LENTH+2)*100. POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"DMA TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/9"); LCD_ShowString(60,130,200,16,16,"KEY0:Start"); //显示提示信息 for(i=0;i<(TEXT_LENTH+2)*100;i++)//填充 ASCII 字符集数据 319 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { if(t>=TEXT_LENTH)//加入换行符 { SendBuff[i++]=0x0d; SendBuff[i]=0x0a; t=0; }else SendBuff[i]=TEXT_TO_SEND[t++];//复制 TEXT_TO_SEND 语句 } POINT_COLOR=BLUE;//设置字体为蓝色 i=0; while(1) { t=KEY_Scan(0); if(t==KEY0_PRES)//KEY0 按下 { LCD_ShowString(60,150,200,16,16,"Start Transimit...."); LCD_ShowString(60,170,200,16,16," %");//显示百分号 printf("\r\nDMA DATA:\r\n "); USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //????1?DMA?? MYDMA_Enable(DMA1_Channel4);//开始一次 DMA 传输! //等待 DMA 传输完成,此时我们来做另外一些事,点灯 //实际应用中,传输数据期间,可以执行另外的任务 while(1) { if(DMA_GetFlagStatus(DMA1_FLAG_TC4)!=RESET)//等待通道 4 传输完成 { DMA_ClearFlag(DMA1_FLAG_TC4);//清除通道 4 传输完成标志 break; } pro=DMA_GetCurrDataCounter(DMA1_Channel4);//当前还剩余数据量 pro=1-pro/((TEXT_LENTH+2)*100);//得到百分比 pro*=100; //扩大 100 倍 LCD_ShowNum(60,170,pro,3,16); } LCD_ShowNum(60,170,100,3,16);//显示 100% LCD_ShowString(60,150,200,16,16,"Transimit Finished!");//传送完成 } i++; delay_ms(10); if(i==20) { LED0=!LED0;//提示系统正在运行 i=0; 320 STM32 不完全手册(库函数版) } } } 至此,DMA 串口传输的软件设计就完成了。 ALIENTEK MiniSTM32 V3.0 开发板教程 23.4 下载验证 在代码编译成功之后,我们通过串口下载代码到 ALIENTEK MiniSTM32 开发板上,可以 看到 DS0 开始闪烁,同时 LCD 显示一些信息,然后我们按 KEY0 按键,开发板就开始通过 DMA 发送数据到串口,并在 TFTLCD 上显示进度等信息,如图 23.4.1 所示: 图 23.4.1 DMA 实验测试图 打开串口调试助手,可以看到串口显示如图 23.4.2 所示的内容: 321 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 23.4.2 串口收到的数据内容 从上图可以看出,我们收到了来自开发板的串口数据。至此,我们整个 DMA 实验就结束 了,希望大家通过本章的学习,掌握 STM32 的 DMA 使用。DMA 是个非常好的功能,它不但 能减轻 CPU 负担,还能提高数据传输速度,合理的应用 DMA,往往能让你的程序设计变得简 单。 322 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二十四章 IIC 实验 本章我们将向大家介绍如何使用 STM32 的普通 IO 口模拟 IIC 时序,并实现和 24C02 之间 的双向通信。在本章中,我们将使用 STM32 的普通 IO 口模拟 IIC 时序,来实现 24C02 的读写, 并将结果显示在 TFTLCD 模块上。本章分为如下几个部分: 24.1 IIC 简介 24.2 硬件设计 24.3 软件设计 24.4 下载验证 323 24.1 IIC 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行总线,用于连接 微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。 在 CPU 与被控 IC 之间、IC 与 IC 之间进行双向传送,高速 IIC 总线一般可达 400kbps 以上。 I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:开始信号、结束信号和应答 信号。 开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。 结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲, 表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接 收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为 受控单元出现故障。 这些信号中,起始信号是必需的,结束信号和应答信号,都可以不要。IIC 总线时序图如 图 24.1..1 所示: 图 24.1.1 IIC 总线时序图 ALIENTEK MiniSTM32 开发板板载的 EEPROM 芯片型号为 24C02。该芯片的总容量是 256 个字节,该芯片通过 IIC 总线与外部连接,我们本章就通过 STM32 来实现 24C02 的读写。 目前大部分 MCU 都带有 IIC 总线接口,STM32 也不例外。但是这里我们不使用 STM32 的硬件 IIC 来读写 24C02,而是通过软件模拟。STM32 的硬件 IIC 非常复杂,更重要的是不稳 定,故不推荐使用。所以我们这里就通过模拟来实现了。有兴趣的读者可以研究一下 STM32 的硬件 IIC。 本章实验功能简介:开机的时候先检测 24C02 是否存在,然后在主循环里面检测两个按键, 其中 1 个按键(WK_UP)用来执行写入 24C02 的操作,另外一个按键(KEY0)用来执行读出 操作,在 TFTLCD 模块上显示相关信息。同时用 DS0 提示程序正在运行。 24.2 硬件设计 本章需要用到的硬件资源有: 1) 指示灯 DS0 2) WK_UP 和 KEY0 按键 3) 串口(USMART 使用) 4) TFTLCD 模块 5) 24C02 324 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 前面 4 部分的资源,我们前面已经介绍了,请大家参考相关章节。这里只介绍 24C02 与 STM32 的连接,24C02 的 SCL 和 SDA 分别连在 STM32 的 PC12 和 PC11 上的,连接关系如图 24.2.1 所示: 图 24.2.1 STM32 与 24C02 连接图 24.3 软件设计 打开 IIC 实验工程,我们可以看到工程中加入了两个源文件分别是 myiic.c 和 24cxx.c, myiic.c 文件存放 iic 驱动代码,24cxx.c 文件存放 24C02 驱动代码: 打开 myiic.c 文件,代码如下: //初始化 IIC void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; //RCC->APB2ENR|=1<<4;//先使能外设 IO PORTC 时钟 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE ); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); IIC_SCL=1; IIC_SDA=1; } //产生 IIC 起始信号 void IIC_Start(void) { SDA_OUT(); IIC_SDA=1; IIC_SCL=1; //sda 线输出 325 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 delay_us(4); IIC_SDA=0;//START:when CLK is high,DATA change form high to low delay_us(4); IIC_SCL=0;//钳住 I2C 总线,准备发送或接收数据 } //产生 IIC 停止信号 void IIC_Stop(void) { SDA_OUT();//sda 线输出 IIC_SCL=0; IIC_SDA=0;//STOP:when CLK is high DATA change form low to high delay_us(4); IIC_SCL=1; IIC_SDA=1;//发送 I2C 总线结束信号 delay_us(4); } //等待应答信号到来 //返回值:1,接收应答失败 // 0,接收应答成功 u8 IIC_Wait_Ack(void) { u8 ucErrTime=0; SDA_IN(); //SDA 设置为输入 IIC_SDA=1;delay_us(1); IIC_SCL=1;delay_us(1); while(READ_SDA) { ucErrTime++; if(ucErrTime>250) { IIC_Stop(); return 1; } } IIC_SCL=0;//时钟输出 0 return 0; } //产生 ACK 应答 void IIC_Ack(void) { IIC_SCL=0; SDA_OUT(); IIC_SDA=0; 326 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 delay_us(2); IIC_SCL=1; delay_us(2); IIC_SCL=0; } //不产生 ACK 应答 void IIC_NAck(void) { IIC_SCL=0; SDA_OUT(); IIC_SDA=1; delay_us(2); IIC_SCL=1; delay_us(2); IIC_SCL=0; } //IIC 发送一个字节 //返回从机有无应答 //1,有应答 //0,无应答 void IIC_Send_Byte(u8 txd) { u8 t; SDA_OUT(); IIC_SCL=0;//拉低时钟开始数据传输 for(t=0;t<8;t++) { IIC_SDA=(txd&0x80)>>7; txd<<=1; delay_us(2); IIC_SCL=1; //对 TEA5767 这三个延时都是必须的 delay_us(2); IIC_SCL=0; delay_us(2); } } //读 1 个字节,ack=1 时,发送 ACK,ack=0,发送 nACK u8 IIC_Read_Byte(unsigned char ack) { unsigned char i,receive=0; SDA_IN();//SDA 设置为输入 for(i=0;i<8;i++ ) { 327 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 IIC_SCL=0; delay_us(2); IIC_SCL=1; receive<<=1; if(READ_SDA)receive++; delay_us(1); } if (!ack) IIC_NAck();//发送 nACK else IIC_Ack(); //发送 ACK return receive; } 该部分为 IIC 驱动代码,实现包括 IIC 的初始化(IO 口)、IIC 开始、IIC 结束、ACK、IIC 读写等功能,在其他函数里面,只需要调用相关的 IIC 函数就可以和外部 IIC 器件通信了,这 里并不局限于 24C02,该段代码可以用在任何 IIC 设备上。 下面我们看看头文件 myiic.h 的代码,里面有两行代码为直接通过寄存器操作设置 IO 口的 模式为输入还是输出,代码如下: #define SDA_IN() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=8<<12;} #define SDA_OUT() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=3<<12;} 其他部分都是一些函数申明之类的,这里不做过多解释。 接下来我们看看 24cxx.c 文件代码: #include "24cxx.h" #include "delay.h" //初始化 IIC 接口 void AT24CXX_Init(void) { IIC_Init(); } //在 AT24CXX 指定地址读出一个数据 //ReadAddr:开始读数的地址 //返回值 :读到的数据 u8 AT24CXX_ReadOneByte(u16 ReadAddr) { u8 temp=0; IIC_Start(); if(EE_TYPE>AT24C16) { IIC_Send_Byte(0XA0); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(ReadAddr>>8);//发送高地址 }else IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); IIC_Wait_Ack(); //发送器件地址 0XA0,写数据 328 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 IIC_Send_Byte(ReadAddr%256); //发送低地址 IIC_Wait_Ack(); IIC_Start(); IIC_Send_Byte(0XA1); //进入接收模式 IIC_Wait_Ack(); temp=IIC_Read_Byte(0); IIC_Stop();//产生一个停止条件 return temp; } //在 AT24CXX 指定地址写入一个数据 //WriteAddr :写入数据的目的地址 //DataToWrite:要写入的数据 void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite) { IIC_Start(); if(EE_TYPE>AT24C16) { IIC_Send_Byte(0XA0); //发送写命令 IIC_Wait_Ack(); IIC_Send_Byte(WriteAddr>>8);//发送高地址 }else IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); IIC_Wait_Ack(); IIC_Send_Byte(WriteAddr%256); //发送低地址 //发送器件地址 0XA0,写数据 IIC_Wait_Ack(); IIC_Send_Byte(DataToWrite); //发送字节 IIC_Wait_Ack(); IIC_Stop(); delay_ms(10); //产生一个停止条件 //对于 EEPROM 器件,每写一次要等待一段时间,否则写失败! } //在 AT24CXX 里面的指定地址开始写入长度为 Len 的数据 //该函数用于写入 16bit 或者 32bit 的数据. //WriteAddr :开始写入的地址 //DataToWrite:数据数组首地址 //Len :要写入数据的长度 2,4 void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len) { u8 t; for(t=0;t>(8*t))&0xff); } //在 AT24CXX 里面的指定地址开始读出长度为 Len 的数据 //该函数用于读出 16bit 或者 32bit 的数据. //ReadAddr :开始读出的地址 //返回值 :数据 329 STM32 不完全手册(库函数版) //Len :要读出数据的长度 2,4 ALIENTEK MiniSTM32 V3.0 开发板教程 u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len) { u8 t; u32 temp=0; for(t=0;t200)return 0; } SPI_I2S_SendData(SPI1, TxData); //通过外设 SPIx 发送一个数据 retry=0; while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET)//检查指定的 SPI 标志位设置与否:接受缓存非空标志位 340 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { retry++; if(retry>200)return 0; } return SPI_I2S_ReceiveData(SPI1); //返回通过 SPIx 最近接收的数据 } 此部分代码主要初始化 SPI,这里我们选择的是 SPI1,所以在 SPI1_Init 函数里面,其相关 的操作都是针对 SPI1 的,其初始化步骤和我们上面介绍步骤 1-5 一样,我们在代码中也使用了 ①~⑤标注。。在初始化之后,我们就可以开始使用 SPI1 了,这里特别注意,SPI 初始化函数的 最后有一个启动传输,这句话最大的作用就是维持 MOSI 为高电平,而且这句话也不是必须的, 可以去掉。 在 SPI1_Init 函数里面,把 SPI1 的频率设置成了最低(36Mhz,256 分频)。在外部函数里 面, 我们通 过 SPI1_SetSpeed 来设 置 SPI1 的速度, 而我们的 数据发送和接 收则是通 过 SPI1_ReadWriteByte 函数来实现的。 对于 spi.h 头文件,里面主要是函数申明,我们就不做过多讲解。 下面我们打开 flash.c,在里面编写与 W25Q64 操作相关的代码,由于篇幅所限,详细代码, 这里就不贴出了。我们仅介绍几个重要的函数,首先是 SPI_Flash_Read 函数,该函数用于从 W25Q64 的指定地址读出指定长度的数据。其代码如下: //读取 SPI FLASH //在指定地址开始读取指定长度的数据 //pBuffer:数据存储区 //ReadAddr:开始读取的地址(24bit) //NumByteToRead:要读取的字节数(最大 65535) void SPI_Flash_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) { u16 i; SPI_FLASH_CS=0; //使能器件 SPI1_ReadWriteByte(W25X_ReadData); //发送读取命令 SPI1_ReadWriteByte((u8)((ReadAddr)>>16)); //发送 24bit 地址 SPI1_ReadWriteByte((u8)((ReadAddr)>>8)); SPI1_ReadWriteByte((u8)ReadAddr); for(i=0;i4096)secremain=4096; //下一个扇区还是写不完 else secremain=NumByteToWrite; //下一个扇区可以写完了 } }; } 该函数可以在 W25Q64 的任意地址开始写入任意长度(必须不超过 W25Q64 的容量)的数 据。我们这里简单介绍一下思路:先获得首地址(WriteAddr)所在的扇区,并计算在扇区内的 偏移,然后判断要写入的数据长度是否超过本扇区所剩下的长度,如果不超过,再先看看是否 要擦除,如果不要,则直接写入数据即可,如果要则读出整个扇区,在偏移处开始写入指定长 342 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 度的数据,然后擦除这个扇区,再一次性写入。当所需要写入的数据长度超过一个扇区的长度 的时候,我们先按照前面的步骤把扇区剩余部分写完,再在新扇区内执行同样的操作,如此循 环,直到写入结束。 其他的代码就比较简单了,我们这里不介绍了。接着打开 flahs.h 文件可以看到,这里面就 定义了一些与 W25Q64 操作相关的命令(部分省略了),这些命令在 W25Q64 的数据手册上都 有详细的介绍,感兴趣的读者可以参考该数据手册,其他的就没啥好说的了。。最后,我们看看 main.c 里面代码如下: //要写入到 W25Q64 的字符串数组 const u8 TEXT_Buffer[]={"WarShipSTM32 SPI TEST"}; #define SIZE sizeof(TEXT_Buffer) int main(void) { u8 key; u16 i=0; u8 datatemp[SIZE]; u32 FLASH_SIZE; delay_init(); //延时函数初始化 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 SPI_Flash_Init(); //SPI FLASH 初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"SPI TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/9"); LCD_ShowString(60,130,200,16,16,"WK_UP:Write KEY0:Read");//显示提示信息 while(SPI_Flash_ReadID()!=W25Q64) //检测不到 W25Q64 { LCD_ShowString(60,150,200,16,16,"25Q64 Check Failed!"); delay_ms(500); LCD_ShowString(60,150,200,16,16,"Please Check! "); delay_ms(500); LED0=!LED0;//DS0 闪烁 } LCD_ShowString(60,150,200,16,16,"25Q64 Ready!"); FLASH_SIZE=8*1024*1024; POINT_COLOR=BLUE; //FLASH 大小为 8M 字节 //设置字体为蓝色 while(1) { key=KEY_Scan(0); if(key==WKUP_PRES) //WK_UP 按下,写入 W25Q64 343 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { LCD_Fill(0,170,239,319,WHITE);//清除半屏 LCD_ShowString(60,170,200,16,16,"Start Write W25Q64...."); SPI_Flash_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE); LCD_ShowString(60,170,200,16,16,"W25Q64 Write Finished!");//提示传送完成 } if(key==KEY0_PRES) //KEY0 按下,读取字符串并显示 { LCD_ShowString(60,170,200,16,16,"Start Read W25Q64.... "); SPI_Flash_Read(datatemp,FLASH_SIZE-100,SIZE);//从指定地址读 SIZE 字节 LCD_ShowString(60,170,200,16,16,"The Data Readed Is: ");//提示传送完成 LCD_ShowString(60,190,200,16,16,datatemp); //显示读到的字符串 } i++; delay_ms(10); if(i==20) { LED0=!LED0; i=0; }//提示系统正在运行 } } 这部分代码和 IIC 实验那部分代码大同小异,我们就不多说了,实现的功能就和 IIC 差不 多,不过此次写入和读出的是 SPI FLASH,而不是 EEPROM。 25.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,通过先按 WK_UP 按键写入数据,然后按 KEY0 读取数据,得到如图 25.4.1 所示: 344 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 25.4.1 SPI 实验程序运行效果图 伴随 DS0 的不停闪烁,提示程序在运行。程序在开机的时候会检测 W25Q64 是否存在,如 果不存在则会在 TFTLCD 模块上显示错误信息,同时 DS0 慢闪。大家可以通过跳线帽把 PA5 和 PA6 短接就可以看到报错了。 345 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二十六章 触摸屏实验 本章,我们将介绍如何使用 STM32 来驱动触摸屏,ALIENTEK MiniSTM32 开发板本身并 没有触摸屏控制器,但是它支持触摸屏,可以通过外接带触摸屏的 LCD 模块(比如 ALIENTEK TFTLCD 模块),来实现触摸屏控制。在本章中,我们将向大家介绍 STM32 控制 ALIENTKE TFTLCD 模块(包括电阻触摸与电容触摸),实现触摸屏驱动,最终实现一个手写板的功能。 本章分为如下几个部分: 26.1 电阻与电容触摸屏简介 26.2 硬件设计 26.3 软件设计 26.4 下载验证 346 26.1 触摸屏简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 目前最常用的触摸屏有两种:电阻式触摸屏与电容式触摸屏。下面,我们来分别介绍。 26.1.1 电阻式触摸屏 在 Iphone 面世之前,几乎清一色的都是使用电阻式触摸屏,电阻式触摸屏利用压力感应进 行触点检测控制,需要直接应力接触,通过检测电阻来定位触摸位置。 ALIENTEK 2.4/2.8/3.5 寸 TFTLCD 模块自带的触摸屏都属于电阻式触摸屏,下面简单介绍 下电阻式触摸屏的原理。 电阻触摸屏的主要部分是一块与显示器表面非常配合的电阻薄膜屏,这是一种多层的复合 薄膜,它以一层玻璃或硬塑料平板作为基层,表面涂有一层透明氧化金属(透明的导电电阻) 导电层,上面再盖有一层外表面硬化处理、光滑防擦的塑料层、它的内表面也涂有一层涂层、 在他们之间有许多细小的(小于 1/1000 英寸)的透明隔离点把两层导电层隔开绝缘。 当手指 触摸屏幕时,两层导电层在触摸点位置就有了接触,电阻发生变化,在 X 和 Y 两个方向上产生 信号,然后送触摸屏控制器。控制器侦测到这一接触并计算出(X,Y)的位置,再根据获得的 位置模拟鼠标的方式运作。这就是电阻技术触摸屏的最基本的原理。 电阻触摸屏的优点:精度高、价格便宜、抗干扰能力强、稳定性好。 电阻触摸屏的缺点:容易被划伤、透光性不太好、不支持多点触摸。 从以上介绍可知,触摸屏都需要一个 AD 转换器, 一般来说是需要一个控制器的。 ALIENTEK TFTLCD 模块选择的是四线电阻式触摸屏,这种触摸屏的控制芯片有很多,包括: ADS7843、ADS7846、TSC2046、XPT2046 和 AK4182 等。这几款芯片的驱动基本上是一样的, 也就是你只要写出了 ADS7843 的驱动,这个驱动对其他几个芯片也是有效的。而且封装也有一 样的,完全 PIN TO PIN 兼容。所以在替换起来,很方便。 ALIENTEK TFTLCD 模块自带的触摸屏控制芯片为 XPT2046。XPT2046 是一款 4 导线制触 摸屏控制器,内含 12 位分辨率 125KHz 转换速率逐步逼近型 A/D 转换器。XPT2046 支持从 1.5V 到 5.25V 的低电压 I/O 接口。XPT2046 能通过执行两次 A/D 转换查出被按的屏幕位置, 除此 之外,还可以测量加在触摸屏上的压力。内部自带 2.5V 参考电压可以作为辅助输入、温度测量 和电池监测模式之用,电池监测的电压范围可以从 0V 到 6V。XPT2046 片内集成有一个温度传 感器。 在 2.7V 的典型工作状态下,关闭参考电压,功耗可小于 0.75mW。XPT2046 采用微小 的封装形式:TSSOP-16,QFN-16(0.75mm 厚度)和 VFBGA-48。工作温度范围为-40℃~+85℃。 该芯片完全是兼容 ADS7843 和 ADS7846 的,关于这个芯片的详细使用,可以参考这两个 芯片的 datasheet。 电阻式触摸屏就介绍到这里。 26.1.2 电容式触摸屏 现在几乎所有智能手机,包括平板电脑都是采用电容屏作为触摸屏,电容屏是利用人体感 应进行触点检测控制,不需要直接接触或只需要轻微接触,通过检测感应电流来定位触摸坐标。 ALIENTEK 4.3/7 寸 TFTLCD 模块自带的触摸屏采用的是电容式触摸屏,下面简单介绍下 电容式触摸屏的原理。 电容式触摸屏主要分为两种: 1、 表面电容式电容触摸屏。 表面电容式触摸屏技术是利用 ITO(铟锡氧化物,是一种透明的导电材料)导电膜,通过电 场感应方式感测屏幕表面的触摸行为进行。但是表面电容式触摸屏有一些局限性,它只能识别 347 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 一个手指或者一次触摸。 2、 投射式电容触摸屏。 投射电容式触摸屏是传感器利用触摸屏电极发射出静电场线。一般用于投射电容传感技术 的电容类型有两种:自我电容和交互电容。 自我电容又称绝对电容,是最广为采用的一种方法,自我电容通常是指扫描电极与地构成 的电容。在玻璃表面有用 ITO 制成的横向与纵向的扫描电极,这些电极和地之间就构成一个电 容的两极。当用手或触摸笔触摸的时候就会并联一个电容到电路中去,从而使在该条扫描线上 的总体的电容量有所改变。在扫描的时候,控制 IC 依次扫描纵向和横向电极,并根据扫描前后 的电容变化来确定触摸点坐标位置。笔记本电脑触摸输入板就是采用的这种方式,笔记本电脑 的输入板采用 X*Y 的传感电极阵列形成一个传感格子,当手指靠近触摸输入板时,在手指和传 感电极之间产生一个小量电荷。采用特定的运算法则处理来自行、列传感器的信号来确定手指 的位置。 交互电容又叫做跨越电容,它是在玻璃表面的横向和纵向的 ITO 电极的交叉处形成电容。 交互电容的扫描方式就是扫描每个交叉处的电容变化,来判定触摸点的位置。当触摸的时候就 会影响到相邻电极的耦合,从而改变交叉处的电容量,交互电容的扫面方法可以侦测到每个交 叉点的电容值和触摸后电容变化,因而它需要的扫描时间与自我电容的扫描方式相比要长一些, 需要扫描检测 X*Y 根电极。目前智能手机/平板电脑等的触摸屏,都是采用交互电容技术。 ALIENTEK 所选择的电容触摸屏,也是采用的是投射式电容屏(交互电容类型),所以后 面仅以投射式电容屏作为介绍。 透射式电容触摸屏采用纵横两列电极组成感应矩阵,来感应触摸。以两个交叉的电极矩阵, 即: X 轴电极和 Y 轴电极,来检测每一格感应单元的电容变化,如图 26.1.2.1 所示: 图 26.1.2.1 投射式电容屏电极矩阵示意图 示意图中的电极,实际是透明的,这里是为了方便大家理解。图中,X、Y 轴的透明电极 电容屏的精度、分辨率与 X、Y 轴的通道数有关,通道数越多,精度越高。以上就是电容触摸 屏的基本原理,接下来看看电容触摸屏的优缺点: 电容触摸屏的优点:手感好、无需校准、支持多点触摸、透光性好。 电容触摸屏的缺点:成本高、精度不高、抗干扰能力差。 这里特别提醒大家电容触摸屏对工作环境的要求是比较高的,在潮湿、多尘、高低温环境 下面,都是不适合使用电容屏的。 电容触摸屏一般都需要一个驱动 IC 来检测电容触摸,且一般是通过 IIC 接口输出触摸数据 348 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 的。ALIENTEK 7’ TFTLCD 模块的电容触摸屏,采用的是 15*10 的驱动结构(10 个感应通道, 15 个驱动通道),采用的是 GT811 做为驱动 IC。ALIENTEK 4.3’ TFTLCD 模块采用的是 13*8 的驱动结构(8 个感应通道,13 个驱动通道),采用的是 OTT2001A 作为驱动 IC。 这两个模块都只支持最多 5 点触摸,在本例程,仅支持 ALIENTEK 4.3 寸 TFTLCD 电容触 摸屏模块,所以这里介绍仅 OTT2001A,GT811 的驱动方法同 OTT2001A 是类似的,大家可以 参考着学习即可。 OTT2001A 是台湾旭曜科技生产的一颗电容触摸屏驱动 IC,最多支持 208 个通道。支持 SPI/IIC 接口,在 ALIENTEK 4.3’ TFTLCD 电容触摸屏上,OTT2001A 只用了 104 个通道,采 用 IIC 接口。IIC 接口模式下,该驱动 IC 与 STM32 的连接仅需要 4 根线:SDA、SCL、RST 和 INT,SDA 和 SCL 是 IIC 通信用的,RST 是复位脚(低电平有效),INT 是中断输出信号,关 于 IIC 我们就不详细介绍了,请参考第二十四章。 OTT2001A 的器件地址为 0X59(不含最低位,换算成读写命令则是读:0XB3,写:0XB2), 接下来,介绍一下 OTT2001A 的几个重要的寄存器。 1, 手势 ID 寄存器 手势 ID 寄存器(00H)用于告诉 MCU,哪些点有效,哪些点无效,从而读取对应的数据, 该寄存器各位描述如表 26.1.2.1 所示: 手势 ID 寄存器(00H) 位 BIT8 BIT6 BIT5 BIT4 说 明 保留 保留 保留 0,(X1,Y1)无效 1,(X1,Y1)有效 位 BIT3 BIT2 BIT1 BIT0 说 0,(X4,Y4)无效 0,(X3,Y3)无效 0,(X2,Y2)无效 0,(X1,Y1)无效 明 1,(X4,Y4)有效 1,(X3,Y3)有效 1,(X2,Y2)有效 1,(X1,Y1)有效 表 26.1.2.1 手势 ID 寄存器 OTT2001A 支持最多 5 点触摸,所以表中只有 5 个位用来表示对应点坐标是否有效,其余 位为保留位(读为 0),通过读取该寄存器,我们可以知道哪些点有数据,哪些点无数据,如果 读到的全是 0,则说明没有任何触摸。 2, 传感器控制寄存器(ODH) 传感器控制寄存器(ODH),该寄存器也是 8 位,仅最高位有效,其他位都是保留,当最 高位为 1 的时候,打开传感器(开始检测),当最高位设置为 0 的时候,关闭传感器(停止检测)。 3, 坐标数据寄存器(共 20 个) 坐标数据寄存器总共有 20 个,每个坐标占用 4 个寄存器,坐标寄存器与坐标的对应关系如 表 26.1.2.2 所示: 寄存器编号 01H 02H 03H 04H 坐标 1 X1[15:8] X1[7:0] Y1[15:8] Y1[7:0] 寄存器编号 05H 06H 07H 08H 坐标 2 X2[15:8] X2[7:0] Y2[15:8] Y2[7:0] 寄存器编号 10H 11H 12H 13H 坐标 3 X3[15:8] X3[7:0] Y3[15:8] Y3[7:0] 寄存器编号 14H 15H 16H 17H 坐标 4 X4[15:8] X4[7:0] Y4[15:8] Y4[7:0] 寄存器编号 18H 19H 1AH 1BH 坐标 5 X5[15:8] X5[7:0] Y5[15:8] Y5[7:0] 349 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 表 26.1.2.2 坐标寄存器与坐标对应表 从表中可以看出,每个坐标的值,可以通过 4 个寄存器读出,比如读取坐标 1(X1,Y1), 我们则可以读取 01H~04H,就可以知道当前坐标 1 的具体数值了,这里我们也可以只发送寄存 器 01,然后连续读取 4 个字节,也可以正常读取坐标 1,寄存器地址会自动增加,从而提高读 取速度。 OTT2001A 相关寄存器的介绍就介绍到这里,更详细的资料,请参考:OTT2001A IIC 协议 指导.pdf 这个文档。OTT2001A 只需要经过简单的初始化就可以正常使用了,初始化流程:复 位延时 100ms释放复位设置传感器控制寄存器的最高位位 1,开启传感器检查。就可以 正常使用了。 最后,OTT2001A 有两个地方需要特别注意一下: 1, OTT2001A 的寄存器是 8 位的,但是发送的时候要发送 16 位(高八位有效),才可 以正常使用。 2, OTT2001A 的输出坐标,默认是以:X 坐标最大值是 2700,Y 坐标最大值是 1500 的分辨率输出的,也就是输出范围为:X:0~2700,Y:0~1500;MCU 在读取到坐 标后,必须根据 LCD 分辨率做一个换算,才能得到真实的 LCD 坐标。 26.2 硬件设计 本章实验功能简介:开机的时候先初始化 LCD,读取 LCD ID,随后,根据 LCD ID 判断 是电阻触摸屏还是电容触摸屏,如果是电阻触摸屏,则先读取 24C02 的数据判断触摸屏是否已 经校准过,如果没有校准,则执行校准程序,校准过后再进入电阻触摸屏测试程序,如果已经 校准了,就直接进入电阻触摸屏测试程序。 如果是电容触摸屏,则执行 OTT2001A 的初始化代码,初始化电容触摸屏,随后进入电容 触摸屏测试程序(电容触摸屏无需校准!!)。 电阻触摸屏测试程序和电容触摸屏测试程序基本一样,只是电容触摸屏支持最多 5 点同时 触摸,电阻触摸屏只支持一点触摸,其他一模一样。测试界面的右上角会有一个清空的操作区 域(RST),点击这个地方就会将输入全部清除,恢复白板状态。使用电阻触摸屏的时候,可 以通过按 KEY0 来实现强制触摸屏校准,只要按下 KEY0 就会进入强制校准程序。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 按键 3) TFTLCD 模块(带电阻/电容式触摸屏) 4) 24C02 所有这些资源与 STM32 的连接图,在前面都已经介绍了,这里我们只针对 TFTLCD 模块 与 STM32 的连接端口再说明一下,TFTLCD 模块的触摸屏(电阻触摸屏)总共有 5 跟线与 STM32 连接,连接电路图如图 26.2.1 所示: 350 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 26.2.1 触摸屏与 STM32 的连接图 从图中可以看出, T_MOSI、T_MISO、T_SCK、T_CS 和 T_PEN 分别连接在 STM32 的:PC3、PC2、 PC0、PC13 和 PC1 上。 如果是电容式触摸屏,我们的接口和电阻式触摸屏一样(上图右侧接口),只是没有用到 五 根 线 了 , 而 是 四 根 线 , 分 别 是 : T_PEN(CT_INT) 、 T_CS(CT_RST) 、 T_CLK(CT_SCL) 和 T_MOSI(CT_SDA)。其中:CT_INT、CT_RST、CT_SCL 和 CT_SDA 分别是 OTT2001A 的:中断输出信 号、复位信号,IIC 的 SCL 和 SDA 信号。这里,我们用查询的方式读取 OTT2001A 的数据,没有 用到中断信号(CT_INT),所以同 STM32 的连接,只需要 3 根线即可。 26.3 软件设计 打开触摸屏实验工程可以发现,我们在工程中添加了 touch.c、touch.h、ctiic.c、ctiic.h、 ott2001a.c 和 ott2001a.h 等六个文件,并保存在 TOUCH 分组下面。其中,touch.c 和 touch.h 是 电阻触摸屏部分的代码,顺带兼电容触摸屏的管理控制,其他则是电容触摸屏部分的代码。 打开 touch.c 文件,在里面输入与触摸屏相关的代码(主要是电阻触摸屏的代码),这里我 们也不全部贴出来了,仅介绍几个重要的函数。 首先我们要介绍的是 TP_Read_XY2 这个函数,该函数专门用于从电阻式触摸屏控制 IC 读取 坐标的值(0~4095),TP_Read_XY2 的代码如下: //连续 2 次读取触摸屏 IC,且这两次的偏差不能超过 //ERR_RANGE,满足条件,则认为读数正确,否则读数错误. //该函数能大大提高准确度 //x,y:读取到的坐标值 //返回值:0,失败;1,成功。 #define ERR_RANGE 50 //误差范围 u8 TP_Read_XY2(u16 *x,u16 *y) { u16 x1,y1; u16 x2,y2; u8 flag; flag=TP_Read_XY(&x1,&y1); if(flag==0)return(0); flag=TP_Read_XY(&x2,&y2); if(flag==0)return(0); if(((x2<=x1&&x11.05||d1==0||d2==0)//不合格 { cnt=0; TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20, WHITE); //清除点 4 TP_Drow_Touch_Point(20,20,RED); //画点 1 TP_Adj_Info_Show(pos_temp[0][0],pos_temp[0][1], pos_temp[1][0],pos_temp[1][1],pos_temp[2][0], pos_temp[2][1],pos_temp[3][0],pos_temp[3][1],fac*100); goto READJ; //不合格,重新校准 } } //正确了 tp_dev.xfac=(float)(lcddev.width-40)/(pos_temp[1][0]pos_temp[0][0]);//得到 xfac tp_dev.xoff=(lcddev.width-tp_dev.xfac*(pos_temp[1][0]+ pos_temp[0][0]))/2;//得到 xoff tp_dev.yfac=(float)(lcddev.height-40)/(pos_temp[2][1]pos_temp[0][1]);//得到 yfac 353 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 tp_dev.yoff=(lcddev.height-tp_dev.yfac*(pos_temp[2][1]+ pos_temp[0][1]))/2;//得到 yoff if(abs(tp_dev.xfac)>2||abs(tp_dev.yfac)>2)//触屏和预设的相反了. { cnt=0; TP_Drow_Touch_Point(lcddev.width-20,lcddev.height-20, WHITE); //清除点 4 TP_Drow_Touch_Point(20,20,RED); //画点 1 LCD_ShowString(40,26,lcddev.width,lcddev.height,16, "TP Need readjust!"); tp_dev.touchtype=!tp_dev.touchtype;//修改触屏类型. if(tp_dev.touchtype)//X,Y 方向与屏幕相反 { CMD_RDX=0X90; CMD_RDY=0XD0; }else { CMD_RDX=0XD0; CMD_RDY=0X90; }//与屏相同 continue; } POINT_COLOR=BLUE; LCD_Clear(WHITE);//清屏 LCD_ShowString(35,110,lcddev.width,lcddev.height,16, "Touch Screen Adjust OK!");//校正完成 delay_ms(1000); TP_Save_Adjdata(); LCD_Clear(WHITE);//清屏 return;//校正完成 } } delay_ms(10); outtime++; if(outtime>1000) { TP_Get_Adjdata();break; } } } TP_Adjust 是此部分最核心的代码,在这里,给大家介绍一下我们这里所使用的触摸屏校 正原理:我们传统的鼠标是一种相对定位系统,只和前一次鼠标的位置坐标有关。而触摸屏则 是一种绝对坐标系统,要选哪就直接点哪,与相对定位系统有着本质的区别。绝对坐标系统的 特点是每一次定位坐标与上一次定位坐标没有关系,每次触摸的数据通过校准转为屏幕上的坐 标,不管在什么情况下,触摸屏这套坐标在同一点的输出数据是稳定的。不过由于技术原理的 原因,并不能保证同一点触摸每一次采样数据相同,不能保证绝对坐标定位,点不准,这就是 触摸屏最怕出现的问题:漂移。对于性能质量好的触摸屏来说,漂移的情况出现并不是很严重。 所以很多应用触摸屏的系统启动后,进入应用程序前,先要执行校准程序。 通常应用程序中使 用的 LCD 坐标是以像素为单位的。比如说:左上角的坐标是一组非 0 的数值,比如(20,20), 而右下角的坐标为(220,300)。这些点的坐标都是以像素为单位的,而从触摸屏中读出的是点 的物理坐标,其坐标轴的方向、XY 值的比例因子、偏移量都与 LCD 坐标不同,所以,需要在 程序中把物理坐标首先转换为像素坐标,然后再赋给 POS 结构,达到坐标转换的目的。 校正思路:在了解了校正原理之后,我们可以得出下面的一个从物理坐标到像素坐标的转 354 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 换关系式: LCDx=xfac*Px+xoff; LCDy=yfac*Py+yoff; 其中(LCDx,LCDy)是在 LCD 上的像素坐标,(Px,Py)是从触摸屏读到的物理坐标。xfac, yfac 分别是 X 轴方向和 Y 轴方向的比例因子,而 xoff 和 yoff 则是这两个方向的偏移量。 这样我们只要事先在屏幕上面显示 4 个点(这四个点的坐标是已知的),分别按这四个点就 可以从触摸屏读到 4 个物理坐标,这样就可以通过待定系数法求出 xfac、yfac、xoff、yoff 这四 个参数。我们保存好这四个参数,在以后的使用中,我们把所有得到的物理坐标都按照这个关 系式来计算,得到的就是准确的屏幕坐标。达到了触摸屏校准的目的。 TP_Adjust 就 是 根 据 上 面 的 原 理 设 计 的 校 准 函 数 , 注 意 该 函 数 里 面 多 次 使 用 了 lcddev.width 和 lcddev.height,用于坐标设置,主要是为了兼容不同尺寸的 LC(D 比如 320*240、 480*320 和 800*480 的屏都可以兼容)。 接下来看看触摸屏初始化函数:TP_Init,该函数根据 LCD 的 ID(即 lcddev.id)判别是电 阻屏还是电容屏,执行不同的初始化,该函数代码如下: //触摸屏初始化 //返回值:0,没有进行校准 // 1,进行过校准 u8 TP_Init(void) { if(lcddev.id==0X5510) //电容触摸屏 { OTT2001A_Init(); tp_dev.scan=CTP_Scan; //扫描函数指向电容触摸屏扫描 tp_dev.touchtype|=0X80; //电容屏 tp_dev.touchtype|=lcddev.dir&0X01;//横屏还是竖屏 return 0; }else { //注意,时钟使能之后,对 GPIO 的操作才有效 //所以上拉之前,必须使能时钟.才能实现真正的上拉输出 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_AFIO, ENABLE);//使能相关时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3|GPIO_Pin_0|GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ; GPIO_Init(GPIOC, &GPIO_InitStructure); TP_Read_XY(&tp_dev.x[0],&tp_dev.y[0]);//第一次读取初始化 AT24CXX_Init();//初始化 24CXX if(TP_Get_Adjdata())return 0;//已经校准 355 STM32 不完全手册(库函数版) else //未校准? ALIENTEK MiniSTM32 V3.0 开发板教程 { LCD_Clear(WHITE);//清屏 TP_Adjust(); //屏幕校准 TP_Save_Adjdata(); } TP_Get_Adjdata(); } return 1; } 该函数比较简单,重点说一下:tp_dev.scan,这个结构体函数指针,默认是指向 TP_Scan 的,如果是电阻屏则用默认的即可,如果是电容屏,则指向新的扫描函数 CTP_Scan,执行电 容触摸屏的扫描函数,CTP_Scan 函数将在后续介绍。 其他的函数我们这里就不多介绍了,接下来打开 touch.h 文件,该文件代码如下: #ifndef __TOUCH_H__ #define __TOUCH_H__ #include "sys.h" #include "ott2001a.h" #define TP_PRES_DOWN 0x80 //触屏被按下 #define TP_CATH_PRES 0x40 //有按键按下了 //触摸屏控制器 typedef struct { u8 (*init)(void); //初始化触摸屏控制器 u8 (*scan)(u8); //扫描触摸屏.0,屏幕扫描;1,物理坐标; void (*adjust)(void); //触摸屏校准 u16 x[OTT_MAX_TOUCH]; //当前坐标 u16 y[OTT_MAX_TOUCH];//电容屏有最多 5 组坐标,电阻屏则用 x[0],y[0]代表: //此次扫描时,触屏的坐标,用 x[4],y[4]存储第一次按下时的坐标. u8 sta; //笔的状态 //b7:按下 1/松开 0; //b6:0,没有按键按下;1,有按键按下. //b5:保留 //b4~b0:电容触摸屏按下的点数(0,未按下,1 按下) float xfac; float yfac; short xoff; short yoff; //新增的参数,当触摸屏的左右上下完全颠倒时需要用到. //b0:0,竖屏(适合左右为 X 坐标,上下为 Y 坐标的 TP) // 1,横屏(适合左右为 Y 坐标,上下为 X 坐标的 TP) //b1~6:保留. //b7:0,电阻屏 356 STM32 不完全手册(库函数版) // 1,电容屏 ALIENTEK MiniSTM32 V3.0 开发板教程 u8 touchtype; }_m_tp_dev; extern _m_tp_dev tp_dev; //与触摸屏芯片连接引脚 //触屏控制器在 touch.c 里面定义 #define PEN PCin(1) //PC1 INT #define DOUT PCin(2) //PC2 MISO #define TDIN PCout(3) //PC3 MOSI #define TCLK PCout(0) //PC0 SCLK #define TCS PCout(13) //PC13 CS //电阻屏函数 void TP_Write_Byte(u8 num); //向控制芯片写入一个数据 u16 TP_Read_AD(u8 CMD); //读取 AD 转换值 u16 TP_Read_XOY(u8 xy); //带滤波的坐标读取(X/Y) u8 TP_Read_XY(u16 *x,u16 *y); //双方向读取(X+Y) u8 TP_Read_XY2(u16 *x,u16 *y); //带加强滤波的双方向坐标读取 void TP_Drow_Touch_Point(u16 x,u16 y,u16 color); //画一个坐标校准点 void TP_Draw_Big_Point(u16 x,u16 y,u16 color); //画一个大点 void TP_Save_Adjdata(void); //保存校准参数 u8 TP_Get_Adjdata(void); //读取校准参数 void TP_Adjust(void); //触摸屏校准 void TP_Adj_Info_Show(u16 x0,u16 y0,u16 x1,u16 y1,u16 x2,u16 y2,u16 x3,u16 y3,u16 fac); //电阻屏/电容屏 共用函数 u8 TP_Scan(u8 tp); //扫描 u8 TP_Init(void); //初始化 #endif 上述代码,我们重点看看_m_tp_dev 结构体,改结构体用于管理和记录触摸屏(包括电阻 触摸屏与电容触摸屏)相关信息,其中:OTT_MAX_TOUCH,是在 ott2001a.h 定义的一个宏, 表示支持的最大触摸点数,多点触摸仅电容屏有效。通过结构体,在使用的时候,我们一般直 接调用 tp_dev 的相关成员函数/变量屏即可达到需要的效果,这种设计简化了接口,且方便管理 和维护,大家可以效仿一下。 ctiic.c 和 ctiic.h 是电容触摸屏的 IIC 接口部分代码,与第二十四章的 myiic.c 和 myiic.h 基本 一样,这里就不单独介绍了,记得把 ctiic.c 加入 HARDWARE 组下。接下来看看:ott2001a.c, 在该文件输入如下代码: //向 OTT2001A 写入一次数据 //reg:起始寄存器地址 //buf:数据缓缓存区 //len:写数据长度 //返回值:0,成功;1,失败. u8 OTT2001A_WR_Reg(u16 reg,u8 *buf,u8 len) { u8 i; u8 ret=0; CT_IIC_Start(); 357 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 CT_IIC_Send_Byte(OTT_CMD_WR);CT_IIC_Wait_Ack(); //发送写命令 CT_IIC_Send_Byte(reg>>8); CT_IIC_Wait_Ack(); //发送高 8 位地址 CT_IIC_Send_Byte(reg&0XFF); CT_IIC_Wait_Ack(); //发送低 8 位地址 for(i=0;i>8); CT_IIC_Wait_Ack(); CT_IIC_Send_Byte(reg&0XFF); CT_IIC_Wait_Ack(); //发送写命令 //发送高 8 位地址 //发送低 8 位地址 CT_IIC_Start(); CT_IIC_Send_Byte(OTT_CMD_RD); CT_IIC_Wait_Ack(); for(i=0;i240)t=10;//重新从 10 开始计数 return res; } 此部分总共 5 个函数,其中 OTT2001A_WR_Reg 和 OTT2001A_RD_Reg 分别用于读写 OTT2001A 芯片,这里特别注意寄存器地址是 16 位的,与 OTT2001A 手册介绍的是有出入的, 必须 16 位才能正常操作。另外,重点介绍下 CTP_Scan 函数,CTP_Scan 函数用于扫描电容触 摸屏是否有按键按下,由于我们不是用的中断方式来读取 OTT2001A 的数据的,而是采用查询 的方式,所以这里使用了一个静态变量来提高效率,当无触摸的时候,尽量减少对 CPU 的占用, 当有触摸的时候,又保证能迅速检测到。至于对 OTT2001A 数据的读取,则完全是我们在上面 介绍的方法,先读取手势 ID 寄存器(OTT_GSTID_REG),判断是不是有有效数据,如果有, 则读取,否则直接忽略,继续后面的处理。 其他的函数我们这里就不多介绍了,保存 ott2001a.c 文件,并把该文件加入到 HARDWARE 组下。接下来打开 ott2001a.h 文件,在该文件里面输入如下代码: //IO 操作函数 #define OTT_RST PCout(13) //OTT2001A 复位引脚 #define OTT_INT PCin(1) //OTT2001A 中断引脚 //通过 OTT_SET_REG 指令,可以查询到这个信息 //注意,这里的 X,Y 和屏幕的坐标系刚好是反的. #define OTT_MAX_X 2700 //TP X 方向的最大值(竖方向) 360 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define OTT_MAX_Y 1500 //TP Y 方向的最大值(横方向) //缩放因子 #define OTT_SCAL_X 0.2963 //屏幕的 纵坐标/OTT_MAX_X #define OTT_SCAL_Y 0.32 //屏幕的 横坐标/OTT_MAX_Y //I2C 读写命令 #define OTT_CMD_WR 0XB2 //写命令 #define OTT_CMD_RD 0XB3 //读命令 //寄存器地址 #define OTT_GSTID_REG 0X0000 //OTT2001A 当前检测到的触摸情况 #define OTT_TP1_REG 0X0100 //第一个触摸点数据地址 #define OTT_TP2_REG 0X0500 //第二个触摸点数据地址 #define OTT_TP3_REG 0X1000 //第三个触摸点数据地址 #define OTT_TP4_REG 0X1400 //第四个触摸点数据地址 #define OTT_TP5_REG 0X1800 //第五个触摸点数据地址 #define OTT_SET_REG 0X0900 //分辨率设置寄存器地址 #define OTT_CTRL_REG 0X0D00 //传感器控制(开/关) #define OTT_MAX_TOUCH 5 //电容屏支持的点数,固定为 5 点 u8 OTT2001A_WR_Reg(u16 reg,u8 *buf,u8 len); //写寄存器(实际无用) void OTT2001A_RD_Reg(u16 reg,u8 *buf,u8 len); //读寄存器 void OTT2001A_SensorControl(u8 cmd); //传感器打开/关闭操作 u8 OTT2001A_Init(void); //4.3 电容触摸屏始化函 这段代码比较简单,重点注意一下 OTT_SCAL_X 和 OTT_SCAL_Y 的由来,前面说了, OTT2001A 输出 X 范围固定为 0~2700,Y 范围固定为:0~1500,所以,要根据我们屏幕的分辨 率(4.3 寸电容屏触摸屏分辨率为:800*480)进行一次换算,得到 LCD 坐标与 OTT2001A 坐标的 比例关系: OTT_SCAL_X=800/2700=0.2963 OTT_SCAL_Y=480/1500=0.32 这样,我们只需要将 OTT2001A 的输出坐标乘以比例因子,就可以得到真实的 LCD 坐 最后我们看看 main.c 文件内容,这里就不全部贴出来了,仅介绍三个重要的函数: //5 个触控点的颜色 //电阻触摸屏测试函数 void rtp_test(void) { u8 key; u8 i=0; while(1) { key=KEY_Scan(0); tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) { //触摸屏被按下 if(tp_dev.x[0](lcddev.width-24)&&tp_dev.y[0]<16)Load_Drow_Dialog() ; 361 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 else TP_Draw_Big_Point(tp_dev.x[0],tp_dev.y[0],RED); //画图 } }else delay_ms(10); //没有按键按下的时候 if(key==KEY0_PRES) //KEY0 按下,则执行校准程序 { LCD_Clear(WHITE);//清屏 TP_Adjust(); //屏幕校准 TP_Save_Adjdata(); Load_Drow_Dialog(); } i++; if(i%20==0)LED0=!LED0; } } const u16 POINT_COLOR_TBL[OTT_MAX_TOUCH]= {RED,GREEN,BLUE,BROWN,GRED}; //电容触摸屏测试函数 void ctp_test(void) { u8 t=0; u8 i=0; u16 lastpos[5][2]; while(1) //最后一次的数据 { tp_dev.scan(0); for(t=0;t(lcddev.width-24)&&tp_dev.y[t]<16) { Load_Drow_Dialog();//清除 } 362 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 } }else lastpos[t][0]=0XFFFF; } delay_ms(5);i++; if(i%20==0)LED0=!LED0; } } int main(void) { delay_init(); //延时函数初始化 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 tp_dev.init(); //触摸屏初始化 POINT_COLOR=RED;//设置字体为红色 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"TOUCH TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/11"); if(tp_dev.touchtype!=0XFF)LCD_ShowString(60,130,200,16,16,"Press KEY0 to Adjust"); delay_ms(1500); Load_Drow_Dialog(); if(tp_dev.touchtype&0X80)ctp_test(); //电容屏测试 else rtp_test(); //电阻屏测试 } 下面分别介绍一下这三个函数。 rtp_test,该函数用于电阻触摸屏的测试,该函数代码比较简单,就是扫描按键和触摸屏, 如果触摸屏有按下,则在触摸屏上面划线,如果按中“RST”区域,则执行清屏。如果按键 KEY0 按下,则执行触摸屏校准。 ctp_test,该函数用于电容触摸屏的测试,由于我们采用 tp_dev.sta 来标记当前按下的触摸 屏点数,所以判断是否有电容触摸屏按下,也就是判断 tp_dev.sta 的最低 5 位,如果有数据, 则划线,如果没数据则忽略,且 5 个点划线的颜色各不一样,方便区分。另外,电容触摸屏不 需要校准,所以没有校准程序。 main 函数,则比较简单,初始化相关外设,然后根据触摸屏类型,去选择执行 ctp_test 还 是 rtp_test。 软件部分就介绍到这里,接下来看看下载验证。 26.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,电阻触摸 屏得到如图 26.4.1 所示界面(左侧画图界面,右侧是校准界面): 363 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 26.4.1 电阻触摸屏测试程序运行效果 左侧的图片,表示已经校准过了,并且可以在屏幕触摸画图了。右侧的图片则是校准界面 程序界面,用于校准触摸屏用(可以按 KEY0 进入校准)。 如果是电容触摸屏,测试界面如图 26.4.2 所示: 图 26.4.2 电容触摸屏测试界面 左侧是单点触摸效果图,右侧是多点触摸(图为 3 点,最大支持 5 点)效果图。 364 第二十七章 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 红外遥控实验 本章,我们将向大家介绍如何通过 STM32 来解码红外遥控器的信号。ALIENTKMiniSTM32 开发板标配了红外接收头和一个小巧的红外遥控器。在本章中,我们将利用 STM32 的输入捕 获功能,解码开发板标配的这个红外遥控器的编码信号,并将解码后的键值 TFTLCD 模块上显 示出来。本章分为如下几个部分: 27.1 红外遥控简介 27.2 硬件设计 27.3 软件设计 27.4 下载验证 365 27.1 红外遥控简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 红外遥控是一种无线、非接触控制技术,具有抗干扰能力强,信息传输可靠,功耗低,成 本低,易实现等显著优点,被诸多电子设备特别是家用电器广泛采用,并越来越多的应用到计 算机系统中。 由于红外线遥控不具有像无线电遥控那样穿过障碍物去控制被控对象的能力,所以,在设 计红外线遥控器时,不必要像无线电遥控器那样,每套(发射器和接收器)要有不同的遥控频率 或编码(否则,就会隔墙控制或干扰邻居的家用电器),所以同类产品的红外线遥控器,可以有 相同的遥控频率或编码,而不会出现遥控信号“串门”的情况。这对于大批量生产以及在家用 电器上普及红外线遥控提供了极大的方面。由于红外线为不可见光,因此对环境影响很小,再 由红外光波动波长远小于无线电波的波长,所以红外线遥控不会影响其他家用电器,也不会影 响临近的无线电设备。 红外遥控的编码方式目前广泛使用的是:PWM(脉冲宽度调制)的 NEC 协议和 Philips PPM(脉冲位置调制) 的 RC-5 协议的。ALIENTEK MiniSTM32 开发板配套的遥控器使用的是 NEC 协议,其特征如下: 1、8 位地址和 8 位指令长度; 2、地址和命令 2 次传输(确保可靠性) 3、PWM 脉冲位置调制,以发射红外载波的占空比代表“0”和“1”; 4、载波频率为 38Khz; 5、位时间为 1.125ms 或 2.25ms; NEC 码的位定义:一个脉冲对应 560us 的连续载波,一个逻辑 1 传输需要 2.25ms(560us 脉冲+1680us 低电平),一个逻辑 0 的传输需要 1.125ms(560us 脉冲+560us 低电平)。而遥控 接收头在收到脉冲的时候为低电平,在没有脉冲的时候为高电平,这样,我们在接收头端收到 的信号为:逻辑 1 应该是 560us 低+1680us 高,逻辑 0 应该是 560us 低+560us 高。 NEC 遥控指令的数据格式为:同步码头、地址码、地址反码、控制码、控制反码。同步码 由一个 9ms 的低电平和一个 4.5ms 的高电平组成,地址码、地址反码、控制码、控制反码均是 8 位数据格式。按照低位在前,高位在后的顺序发送。采用反码是为了增加传输的可靠性(可 用于校验)。 我们遥控器的按键▽按下时,从红外接收头端收到的波形如图 27.1.1 所示: 图 27.1.1 按键 2 所对应的红外波形 从图 27.1.1 中可以看到,其地址码为 0,控制码为 168。可以看到在 100ms 之后,我们还 收到了几个脉冲,这是 NEC 码规定的连发码(由 9ms 低电平+2.5m 高电平+0.56ms 低电平 +97.94ms 高电平组成),如果在一帧数据发送完毕之后,按键仍然没有放开,则发射重复码, 即连发码,可以通过统计连发码的次数来标记按键按下的长短/次数。 第十四章我们曾经介绍过利用输入捕获来测量高电平的脉宽,本章解码红外遥控信号,刚 好可以利用输入捕获的这个功能来实现遥控解码。关于输入捕获的介绍,请参考第十四章的内 容。 366 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 27.2 硬件设计 本实验采用定时器的输入捕获功能实现红外解码,本章实验功能简介:开机在 LCD 上显示 一些信息之后,即进入等待红外触发,如过接收到正确的红外信号,则解码,并在 LCD 上显示 键值和所代表的意义,以及按键次数等信息。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块(带触摸屏) 3) 红外接收头 4) 红外遥控器 前两个,在之前的实例已经介绍过了,遥控器属于外部器件,遥控接收头在板子上,与 MCU 的连接原理图如 27.2.1 所示: 图 27.2.1 红外遥控接收头与 STM32 的连接电路图 红外遥控接收头通过 P2 与 P3,连接在 STM32 的 PA1(TIM5_CH2)上。硬件上,我们只 需要拿一个跳线帽把 RMT 和 PA1 短接即可(默认已经短接)。然后,程序将 TIM5_CH2 设计 为输入捕获,然后将收到的脉冲信号解码就可以了。 开发板配套的红外遥控器外观如图 27.2.2 所示: 图 27.2.2 红外遥控器 27.3 软件设计 打开我们光盘的红外遥控器实验工程,可以看到我们添加了 remote.c 和 remote.h 两个文件, 同时因为我们使用的是输入捕获,所以还用到库函数 stm32f10x_tim.c 和头文件 stm32f10x_tim.h。 打开 remote.c 文件,代码如下: #include "remote.h" 367 #include "delay.h" #include "usart.h" //红外遥控初始化 //设置 IO 以及定时器 4 的输入捕获 void Remote_Init(void) { STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_InitTypeDef GPIO_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能 PORTB 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE); //TIM5 时钟使能 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_1); //初始化 GPIOA1 //PA1 输入 //上拉输入 TIM_TimeBaseStructure.TIM_Period = 10000; //设定计数器自动重装值 最大 10ms 溢出 TIM_TimeBaseStructure.TIM_Prescaler =(72-1); //预分频器 1M 的计数频率,1us 加 1. TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式 TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; // IC2 映射到 TI5 上 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频 TIM_ICInitStructure.TIM_ICFilter = 0x03; //IC4F=0011 配置输入滤波器 8 个定时器时钟周期滤波 TIM_ICInit(TIM5, &TIM_ICInitStructure);//初始化定时器输入捕获通道 TIM_Cmd(TIM5,ENABLE ); //使能定时器 5 NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //TIM5 中断 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //先占优先级 0 级 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //从优先级 3 级 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ 通道被使能 NVIC_Init(&NVIC_InitStructure); //初始化外设 NVIC 寄存器 368 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 TIM_ITConfig( TIM5,TIM_IT_Update|TIM_IT_CC2,ENABLE); //允许更新中断 ,允许 CC2IE 捕获中断 } //遥控器接收状态 //[7]:收到了引导码标志 //[6]:得到了一个按键的所有信息 //[5]:保留 //[4]:标记上升沿是否已经被捕获 //[3:0]:溢出计时器 u8 RmtSta=0; u16 Dval; //下降沿时计数器的值 u32 RmtRec=0; //红外接收到的数据 u8 RmtCnt=0; //按键按下的次数 //定时器 5 中断服务程序 void TIM5_IRQHandler(void) { if(TIM_GetITStatus(TIM5,TIM_IT_Update)!=RESET) { if(RmtSta&0x80)//上次有数据被接收到了 { RmtSta&=~0X10; //取消上升沿已经被捕获标记 if((RmtSta&0X0F)==0X00)RmtSta|=1<<6; //标记已经完成一次按键的键值信息采集 if((RmtSta&0X0F)<14)RmtSta++; else { RmtSta&=~(1<<7);//清空引导标识 RmtSta&=0XF0; //清空计数器 } } } if(TIM_GetITStatus(TIM5,TIM_IT_CC2)!=RESET) { if(RDATA)//上升沿捕获 { TIM_OC2PolarityConfig(TIM5,TIM_ICPolarity_Falling); //下降沿捕获 TIM_SetCounter(TIM5,0); //清空定时器值 RmtSta|=0X10; }else //下降沿捕获 //标记上升沿已经被捕获 { Dval=TIM_GetCapture2(TIM5);//读取 CCR1 也可以清 CC1IF 标志位 369 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 TIM_OC2PolarityConfig(TIM5,TIM_ICPolarity_Rising); //上升沿捕获 if(RmtSta&0X10) //完成一次高电平捕获 { if(RmtSta&0X80)//接收到了引导码 { if(Dval>300&&Dval<800) //560 为标准值,560us { RmtRec<<=1; //左移一位. RmtRec|=0; //接收到 0 }else if(Dval>1400&&Dval<1800) //1680 为标准值,1680us { RmtRec<<=1; //左移一位. RmtRec|=1; //接收到 1 }else if(Dval>2200&&Dval<2600) //得到按键键值增加的信息 2500 为标准值 2.5ms { RmtCnt++; //按键次数增加 1 次 RmtSta&=0XF0; //清空计时器 } }else if(Dval>4200&&Dval<4700) //4500 为标准值 4.5ms { RmtSta|=1<<7; RmtCnt=0; //标记成功接收到了引导码 //清除按键次数计数器 } } RmtSta&=~(1<<4); } } TIM_ClearFlag(TIM5,TIM_IT_Update|TIM_IT_CC2); } //处理红外键盘 //返回值: // 0,没有任何按键按下 //其他,按下的按键键值. u8 Remote_Scan(void) { u8 sta=0; u8 t1,t2; if(RmtSta&(1<<6))//得到一个按键的所有信息了 { 370 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 t1=RmtRec>>24; //得到地址码 t2=(RmtRec>>16)&0xff; //得到地址反码 if((t1==(u8)~t2)&&t1==REMOTE_ID)//检验遥控识别码(ID)及地址 { t1=RmtRec>>8; t2=RmtRec; if(t1==(u8)~t2)sta=t1;//键值正确 } if((sta==0)||((RmtSta&0X80)==0))//按键数据错误/遥控已经没有按下了 { RmtSta&=~(1<<6);//清除接收到有效按键标识 RmtCnt=0; //清除按键次数计数器 } } return sta; } 该部分代码包含 3 个函数,首先是 Remote_Init 函数,该函数用于初始化 IO 口,并配置 TIM5_CH2 为输入捕获,并设置其相关参数。TIM5_IRQHandler 函数是 TIM5 的中断服务函数, 在该函数里面,实现对红外信号的高电平脉冲的捕获,同时根据我们之前简介的协议内容来解 码 ,该函数用到几个全局变量,用于辅助解码,并存储解码结果。 这里简单介绍一下高电平捕获思路:首先输入捕获设置的是捕获上升沿,在上升沿捕获到 以后,立即设置输入捕获模式为捕获下降沿(以便捕获本次高电平),然后,清零定时器的计数 器值,并标记捕获到上升沿。当下降沿到来时,再次进入捕获中断服务函数,立即更改输入捕 获模式为捕获上升沿(以便捕获下一次高电平),然后处理此次捕获到的高电平。 最后是 Remote_Scan 函数,该函用来扫描解码结果,相当于我们的按键扫描,输入捕获解 码的红外数据,通过该函数传送给其他程序。 接下来我们看看头文件 remote.h 的内容: #ifndef __RED_H #define __RED_H #include "sys.h" #define RDATA PAin(1) //红外数据输入脚 //红外遥控识别码(ID),每款遥控器的该值基本都不一样,但也有一样的. //我们选用的遥控器识别码为 0 #define REMOTE_ID 0 extern u8 RmtCnt; //按键按下的次数 void Remote_Init(void); //红外传感器接收头引脚初始化 u8 Remote_Scan(void); #endif 这里的 REMOTE_ID 就是我们开发板配套的遥控器的识别码,对于其他遥控器可能不一样, 只要修改这个为你所使用的遥控器的一致就可以了。其他是一些函数的声明。最后我们来看看 主函数代码: int main(void) { 371 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 u8 key; u8 t=0; u8 *str=0; delay_init(); //延时函数初始? NVIC_Configuration(); uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); Remote_Init(); //红外接收初始化 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"REMOTE TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/12"); LCD_ShowString(60,130,200,16,16,"KEYVAL:"); LCD_ShowString(60,150,200,16,16,"KEYCNT:"); LCD_ShowString(60,170,200,16,16,"SYMBOL:"); while(1) { key=Remote_Scan(); if(key) { LCD_ShowNum(116,130,key,3,16); //显示键值 LCD_ShowNum(116,150,RmtCnt,3,16); //显示按键次数 switch(key) { case 0:str="ERROR";break; case 162:str="POWER";break; case 98:str="UP";break; case 2:str="PLAY";break; case 226:str="ALIENTEK";break; case 194:str="RIGHT";break; case 34:str="LEFT";break; case 224:str="VOL-";break; case 168:str="DOWN";break; case 144:str="VOL+";break; case 104:str="1";break; case 152:str="2";break; case 176:str="3";break; case 48:str="4";break; case 24:str="5";break; case 122:str="6";break; case 16:str="7";break; 372 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 case 56:str="8";break; case 90:str="9";break; case 66:str="0";break; case 82:str="DELETE";break; } LCD_Fill(116,170,116+8*8,170+16,WHITE); LCD_ShowString(116,170,200,16,16,str); //清楚之前的显示 //显示 SYMBOL }else delay_ms(10); t++; if(t==20) { t=0; LED0=!LED0; } } } main 函数代码比较简单,主要是通过 Remote_Scan 函数获得红外遥控输入的数据(键值), 然后显示在 LCD 上面。 至此,我们的软件设计部分就结束了。 27.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 显示如图 27.4.1 所示的内容: 图 27.4.1 程序运行效果图 此时我们通过遥控器按下不同的按键,则可以看到 LCD 上显示了不同按键的键值以及按键 次数和对应的遥控器上的符号。如图 27.4.2 所示: 373 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 27.4.2 解码成功 374 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第二十八章 DS18B20 数字温度传感器实验 STM32 虽然内部自带了温度传感器,但是因为芯片温升较大等问题,与实际温度差别较大, 所以,本章我们将向大家介绍如何通过 STM32 来读取外部数字温度传感器的温度,来得到较 为准确的环境温度。在本章中,我们将学习使用单总线技术,通过它来实现 STM32 和外部温 度传感器(DS18B20)的通信,并把从温度传感器得到的温度显示在 TFTLCD 模块上。本章分 为如下几个部分: 28.1 DS18B20 简介 28.2 硬件设计 28.3 软件设计 28.4 下载验证 375 28.1 DS18B20 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 DS18B20 是由 DALLAS 半导体公司推出的一种的“一线总线”接口的温度传感器。与传 统的热敏电阻等测温元件相比,它是一种新型的体积小、适用电压宽、与微处理器接口简单的 数字化温度传感器。一线总线结构具有简洁且经济的特点,可使用户轻松地组建传感器网络, 从而为测量系统的构建引入全新概念,测量温度范围为-55~+125℃ ,精度为±0.5℃。现场温 度直接以“一线总线”的数字方式传输,大大提高了系统的抗干扰性。它能直接读出被测温度, 并且可根据实际要求通过简单的编程实现 9~l2 位的数字值读数方式。它工作在 3~5.5 V 的电压 范围,采用多种封装形式,从而使系统设计灵活、方便,设定分辨率及用户设定的报警温度存 储在 EEPROM 中,掉电后依然保存。其内部结构如图 28.1.1 所示: 图 28.1.1 DS18B20 内部结构图 ROM 中的 64 位序列号是出厂前被光记好的,它可以看作是该 DS18B20 的地址序列码,每 DS18B20 的 64 位序列号均不相同。64 位 ROM 的排列是:前 8 位是产品家族码,接着 48 位是 DS18B20 的序列号,最后 8 位是前面 56 位的循环冗余校验码(CRC=X8+X5 +X4 +1)。ROM 作 用是使每一个 DS18B20 都各不相同,这样就可实现一根总线上挂接多个。 所有的单总线器件要求采用严格的信号时序,以保证数据的完整性。DS18B20 共有 6 种信 号类型:复位脉冲、应答脉冲、写 0、写 1、读 0 和读 1。所有这些信号,除了应答脉冲以外, 都由主机发出同步信号。并且发送所有的命令和数据都是字节的低位在前。这里我们简单介绍 这几个信号的时序: 1)复位脉冲和应答脉冲 单总线上的所有通信都是以初始化序列开始。主机输出低电平,保持低电平时间至少 480 us,,以产生复位脉冲。接着主机释放总线,4.7K 的上拉电阻将单总线拉高,延时 15~60 us, 并进入接收模式(Rx)。接着 DS18B20 拉低总线 60~240 us,以产生低电平应答脉冲, 若为低电平,再延时 480 us。 2)写时序 写时序包括写 0 时序和写 1 时序。所有写时序至少需要 60us,且在 2 次独立的写时序之间 至少需要 1us 的恢复时间,两种写时序均起始于主机拉低总线。写 1 时序:主机输出低电平, 延时 2us,然后释放总线,延时 60us。写 0 时序:主机输出低电平,延时 60us,然后释放总线, 延时 2us。 3)读时序 单总线器件仅在主机发出读时序时,才向主机传输数据,所以,在主机发出读数据命令后, 必须马上产生读时序,以便从机能够传输数据。所有读时序至少需要 60us,且在 2 次独立的读 376 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 时序之间至少需要 1us 的恢复时间。每个读时序都由主机发起,至少拉低总线 1us。主机在读 时序期间必须释放总线,并且在时序起始后的 15us 之内采样总线状态。典型的读时序过程为: 主机输出低电平延时 2us,然后主机转入输入模式延时 12us,然后读取单总线当前的电平,然 后延时 50us。 在了解了单总线时序之后,我们来看看 DS18B20 的典型温度读取过程,DS18B20 的典型 温度读取过程为:复位发 SKIP ROM 命令(0XCC)发开始转换命令(0X44)延时复 位发送 SKIP ROM 命令(0XCC)发读存储器命令(0XBE)连续读出两个字节数据(即 温度)结束。 DS18B20 的介绍就到这里,更详细的介绍,请大家参考 DS18B20 的技术手册。 28.2 硬件设计 由于开发板上标准配置是没有 DS18B20 这个传感器的,只有接口,所以要做本章的实验, 大家必须找一个 DS18B20 插在预留的 18B20 接口上。 本章实验功能简介:开机的时候先检测是否有 DS18B20 存在,如果没有,则提示错误。 只有在检测到 DS18B20 之后才开始读取温度并显示在 LCD 上,如果发现了 DS18B20,则程 序每隔 100ms 左右读取一次数据,并把温度显示在 LCD 上。同样我们也是用 DS0 来指示程序 正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) DS18B20 温度传感器 前两部分,在之前的实例已经介绍过了,而DS18B20温度传感器属于外部器件(板上没有 直接焊接),但是在我们开发板上是有DS18B20接口(U6)的,直接插上DS18B20即可使用。 下面,我们介绍开发板上DS18B20接口和STM32的连接电路,如图28.2.1所示: 图 28.2.1 DS18B20 接口与 STM32 的连接电路图 从上图可以看出,我们使用的是 STM32 的 PA0 来连接 DS18B20 的(U6)的 DQ 引脚,图中 U6 为 DS18B20 的插口(3 脚圆孔座)。将 DS18B20 传感器插入到这个上面,并用跳线帽短接 1820 与 PA0,就可以通过 STM32 来读取 DS18B20 的温度了。连接示意图如图 28.2.2 所示: 377 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 28.2.2 DS18B20 连接示意图 从上图可以看出,DS18B20 的平面部分(有字的那面)应该朝外,而曲面部分朝内。然后 插入如图所示的三个孔内。 28.3 软件设计 打开我们的 DS18B20 数字温度传感器实验工程可以看到我们添加了 ds18b20.c 文件以及其 头文件 ds18b20.h 文件,所有 ds18b20 驱动代码和相关定义都分布在这两个文件中。 打开 ds18b20.c,该文件代码如下: #include "ds18b20.h" #include "delay.h" //复位 DS18B20 void DS18B20_Rst(void) { DS18B20_IO_OUT(); //SET PA0 OUTPUT DS18B20_DQ_OUT=0; //拉低 DQ delay_us(750); //拉低 750us DS18B20_DQ_OUT=1; //DQ=1 delay_us(15); //15US } //等待 DS18B20 的回应 //返回 1:未检测到 DS18B20 的存在 //返回 0:存在 u8 DS18B20_Check(void) { u8 retry=0; DS18B20_IO_IN();//SET PA0 INPUT while (DS18B20_DQ_IN&&retry<200) { retry++; delay_us(1); }; if(retry>=200)return 1; else retry=0; while (!DS18B20_DQ_IN&&retry<240) { retry++; delay_us(1); }; if(retry>=240)return 1; return 0; } 378 STM32 不完全手册(库函数版) //从 DS18B20 读取一个位 //返回值:1/0 ALIENTEK MiniSTM32 V3.0 开发板教程 u8 DS18B20_Read_Bit(void) { u8 data; DS18B20_IO_OUT(); //SET PA0 OUTPUT DS18B20_DQ_OUT=0; delay_us(2); DS18B20_DQ_OUT=1; DS18B20_IO_IN(); //SET PA0 INPUT delay_us(12); if(DS18B20_DQ_IN)data=1; else data=0; delay_us(50); return data; } //从 DS18B20 读取一个字节 //返回值:读到的数据 u8 DS18B20_Read_Byte(void) { u8 i,j,dat=0; for (i=1;i<=8;i++) { j=DS18B20_Read_Bit(); dat=(j<<7)|(dat>>1); } return dat; } //写一个字节到 DS18B20 //dat:要写入的字节 void DS18B20_Write_Byte(u8 dat) { u8 j; u8 testb; DS18B20_IO_OUT();//SET PA0 OUTPUT; for (j=1;j<=8;j++) { testb=dat&0x01; dat=dat>>1; if (testb) { DS18B20_DQ_OUT=0;// Write 1 delay_us(2); DS18B20_DQ_OUT=1; 379 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 delay_us(60); } else { DS18B20_DQ_OUT=0;// Write 0 delay_us(60); DS18B20_DQ_OUT=1; delay_us(2); } } } //开始温度转换 void DS18B20_Start(void)// ds1820 start convert { DS18B20_Rst(); DS18B20_Check(); DS18B20_Write_Byte(0xcc);// skip rom DS18B20_Write_Byte(0x44);// convert } //初始化 DS18B20 的 IO 口 DQ 同时检测 DS 的存在 //返回 1:不存在 //返回 0:存在 u8 DS18B20_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//使能 PORTA 时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //PORTA0 推挽输出 GPIO_SetBits(GPIOA,GPIO_Pin_0); DS18B20_Rst(); return DS18B20_Check(); } //从 ds18b20 得到温度值 //精度:0.1C //返回值:温度值 (-550~1250) short DS18B20_Get_Temp(void) { u8 temp; u8 TL,TH; short tem; DS18B20_Start (); //输出 1 // ds1820 start convert 380 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 DS18B20_Rst(); DS18B20_Check(); DS18B20_Write_Byte(0xcc);// skip rom DS18B20_Write_Byte(0xbe);// convert TL=DS18B20_Read_Byte(); // LSB TH=DS18B20_Read_Byte(); // MSB if(TH>7) { TH=~TH; TL=~TL; temp=0;//温度为负 }else temp=1;//温度为正 tem=TH; //获得高八位 tem<<=8; tem+=TL;//获得底八位 tem=(float)tem*0.625;//转换 if(temp)return tem; //返回温度值 else return -tem; } 该部分代码就是根据我们前面介绍的单总线操作时序来读取 DS18B20 的温度值的,DS18B20 的温度通过 DS18B20_Get_Temp 函数读取,该函数的返回值为带符号的短整形数据,返回值的 范围为-550~1250,其实就是温度值扩大了 10 倍。 接下来我们看看 ds18b20.h 文件的内容: #ifndef __DS18B20_H #define __DS18B20_H #include "sys.h" //IO 方向设置 #define DS18B20_IO_IN() {GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=8<<0;} #define DS18B20_IO_OUT() {GPIOA->CRL&=0XFFFFFFF0;GPIOA->CRL|=3<<0;} ////IO 操作函数 #define DS18B20_DQ_OUT PAout(0) //数据端口 PA0 #define DS18B20_DQ_IN PAin(0) //数据端口 PA0 u8 DS18B20_Init(void); //初始化 DS18B20 short DS18B20_Get_Temp(void); //获取温度 void DS18B20_Start(void); //开始温度转换 void DS18B20_Write_Byte(u8 dat); //写入一个字节 u8 DS18B20_Read_Byte(void); //读出一个字节 u8 DS18B20_Read_Bit(void); //读出一个位 u8 DS18B20_Check(void); void DS18B20_Rst(void); //检测是否存在 DS18B20 //复位 DS18B20 #endif #endif 关于这段代码,我们就不做多的解释了,相信大家都不陌生了。然后打开 main.c,主函数 代码如下: 381 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 int main(void) { u8 t=0; short temperature; delay_init(); uart_init(9600); LED_Init(); //延时函数初始化 //串口初始化为 9600 //初始化与 LED 连接的硬件接口 LCD_Init(); POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"DS18B20 TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/12"); while(DS18B20_Init()) //DS18B20 初始化 { LCD_ShowString(60,130,200,16,16,"DS18B20 Error"); delay_ms(200); LCD_Fill(60,130,239,130+16,WHITE); delay_ms(200); } LCD_ShowString(60,130,200,16,16,"DS18B20 OK"); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"Temp: . C"); while(1) { if(t%10==0)//每 100ms 读取一次 { temperature=DS18B20_Get_Temp(); if(temperature<0) { LCD_ShowChar(60+40,150,'-',16,0); //显示负号 temperature=-temperature; //转为正数 }else LCD_ShowChar(60+40,150,' ',16,0); //去掉负号 LCD_ShowNum(60+40+8,150,temperature/10,2,16); //显示正数部分 LCD_ShowNum(60+40+32,150,temperature%10,1,16); //显示小数部分 } delay_ms(10); t++; if(t==20) {t=0;LED0=!LED0;} } 至此,我们本章的软件设计就结束了。 28.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 显示开始显示当前的温度值(假定 DS18B20 已经接上去了,并且 PA0 和 1820 的跳线帽已 382 经短接),如图 28.4.1 所示: STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 28.4.1 DS18B20 实验效果图 该程序还可以读取并显示负温度值的,只是由于本人在广州,是没办法看到了(除非放到 冰箱),具备条件的朋友可以测试一下。 383 第二十九章 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 无线通信实验 ALIENTKEMiniSTM32 开发板带有一个 2.4G 无线模块(NRF24L01 模块)通信接口,采用 8 脚插针方式与开发板连接。本章我们将以 NRF24L01 模块为例向大家介绍如何在 ALIENTEK MiniSTM32 开发板上实现无线通信。在本章中,我们将使用两块 MiniSTM32 开发板,一块用 于发送收据,另外一块用于接收,从而实现无线数据传输。本章分为如下几个部分: 29.1 NRF24L01 无线模块简介 29.2 硬件设计 29.3 软件设计 29.4 下载验证 384 29.1 NRF24L01 无线模块简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 NRF24L01 无线模块,采用的芯片是 NRF24L01,该芯片的主要特点如下: 1)2.4G 全球开放的 ISM 频段,免许可证使用。 2)最高工作速率 2Mbps,高校的 GFSK 调制,抗干扰能力强。 3)125 个可选的频道,满足多点通信和调频通信的需要。 4)内置 CRC 检错和点对多点的通信地址控制。 5)低工作电压(1.9~3.6V)。 6)可设置自动应答,确保数据可靠传输。 该芯片通过 SPI 与外部 MCU 通信,最大的 SPI 速度可以达到 10Mhz。本章我们用到的模 块是深圳云佳科技生产的 NRF24L01,该模块已经被很多公司大量使用,成熟度和稳定性都是 相当不错的。该模块的外形和引脚图如图 29.1.1 所示: 图 29.1.1 NRF24L01 无线模块外观引脚图 模块 VCC 脚的电压范围为 1.9~3.6V,建议不要超过 3.6V,否则可能烧坏模块,一般用 3.3V 电压比较合适。除了 VCC 和 GND 脚,其他引脚都可以和 5V 单片机的 IO 口直连,正是因为其 兼容 5V 单片机的 IO,故使用上具有很大优势。 关于 NRF24L01 的详细介绍,请参考 NRF24L01 的技术手册。 29.2 硬件设计 本章实验功能简介:开机的时候先检测 NRF24L01 模块是否存在,在检测到 NRF24L01 模块之后,根据 KEY0 和 KEY1 的设置来决定模块的工作模式,在设定好工作模式之后,就会 不停的发送/接收数据,同样用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 和 KEY1 按键 3) TFTLCD 模块 4) NRF24L01 模块 NRF24L01 模块属于外部模块,这里我们仅介绍开发板上 NRF24L01 模块接口和 STM32 的连接情况,他们的连接关系如图 29.2.1 所示: 385 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 29.2.1 NRF24L01 模块接口与 STM32 连接原理图 这里NRF24L01也是使用的SPI1,和W25Q64以及SD卡等共用一个SPI接口,所以在使用 的时候,他们分时复用SPI1。本章我们需要把SD卡和W25Q64的片选信号置高,以防止这两个 器件对NRF24L01的通信造成干扰。 由于无线通信实验是双向的,所以至少要有两个模块同时工作才可以,这里我们使用2套 ALIENTEK MiniSTM32开发板来向大家演示。 29.3 软件设计 打开我们的无线通信实验项目工程,可以看到我们加入了 24l01.c 文件和 24l01.h 头文件, 所有 24L01 相关的驱动代码和定义都在这两个文件中实现。同时,我们还加入了之前的 spi 驱 动文件 spi.c 和 spi.h 头文件,因为 24L01 是通过 SPI 接口通信的。 打开 24l01.c 文件,输入如下代码: void NRF24L01_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC, ENABLE ); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); GPIO_SetBits(GPIOC,GPIO_Pin_4); //推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU ; //上拉输入 386 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4); SPI1_Init(); //初始化 SPI SPI_Cmd(SPI1, DISABLE); // SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置 SPI 单向或者双向的数据模式:SPI 设置为双线双向全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置为主 SPI SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8 位帧结构 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //时钟悬空低电平 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //数据捕获于第一个时钟沿 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //内部 NSS 信号有 SSI 位控制 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为 256 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //数据传输从 MSB 位开始 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC 值计算的多项式 SPI_Init(SPI1, &SPI_InitStructure); //初始化外设 SPIx 寄存器 NRF24L01_CE=0; //使能 24L01 NRF24L01_CSN=1; //SPI 片选取消 }//检测 24L01 是否存在 //返回值:0,成功;1,失败 u8 NRF24L01_Check(void) { u8 buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5}; u8 i; SPI2_SetSpeed(SPI_BaudRatePrescaler_8); //spi 速度为 9Mhz NRF24L01_Write_Buf(WRITE_REG+TX_ADDR,buf,5);//写入 5 个字节的地址. NRF24L01_Read_Buf(TX_ADDR,buf,5); //读出写入的地址 for(i=0;i<5;i++)if(buf[i]!=0XA5)break; if(i!=5)return 1;//检测 24L01 错误 return 0; //检测到 24L01 } //SPI 写寄存器 //reg:指定寄存器地址 //value:写入的值 u8 NRF24L01_Write_Reg(u8 reg,u8 value) { u8 status; NRF24L01_CSN=0; //使能 SPI 传输 status =SPI2_ReadWriteByte(reg);//发送寄存器号 387 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 SPI2_ReadWriteByte(value); //写入寄存器的值 NRF24L01_CSN=1; //禁止 SPI 传输 return(status); //返回状态值 } //读取 SPI 寄存器值 //reg:要读的寄存器 u8 NRF24L01_Read_Reg(u8 reg) { u8 reg_val; NRF24L01_CSN = 0; //使能 SPI 传输 SPI2_ReadWriteByte(reg); //发送寄存器号 reg_val=SPI2_ReadWriteByte(0XFF);//读取寄存器内容 NRF24L01_CSN = 1; //禁止 SPI 传输 return(reg_val); //返回状态值 } //在指定位置读出指定长度的数据 //reg:寄存器(位置) //*pBuf:数据指针 //len:数据长度 //返回值,此次读到的状态寄存器值 u8 NRF24L01_Read_Buf(u8 reg,u8 *pBuf,u8 len) { u8 status,u8_ctr; NRF24L01_CSN = 0; //使能 SPI 传输 status=SPI2_ReadWriteByte(reg);//发送寄存器值(位置),并读取状态值 for(u8_ctr=0;u8_ctr('~'))key=' '; tmp_buf[t]=key; } mode++; if(mode>'~')mode=' '; tmp_buf[32]=0;//加入结束符 }else { LCD_ShowString(60,170,239,32,16,"Send Failed "); LCD_Fill(0,188,240,218,WHITE);//清空上面的显示 }; 393 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LED0=!LED0; delay_ms(1500); }; } 以上代码,我们就实现了 29.2 节所介绍的功能,程序运行时先通过 NRF24L01_Check 函 数检测 NRF24L01 是否存在,如果存在,则让用户选择发送模式(KEY1)还是接收模式(KEY0), 在确定模式之后,设置 NRF24L01 的工作模式,然后执行相应的数据发送/接收处理。 至此,我们整个实验的软件设计就完成了。 29.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 显示如图 29.4.1 所示的内容(默认 NRF24L01 已经接上了): 图 29.4.1 选择工作模式界面 通过 KEY0 和 KEY1 来选择 NRF24L01 模块所要进入的工作模式,我们两个开发板一个选 择发送,一个选择接收就可以了。 设置好后通信界面如图 29.4.2 所示: 394 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 29.4.2 通信界面 上图中,左侧的图片来自开发板 A,工作在发送模式。右侧的图片来自开发板 B,工作在 接收模式,A 发送,B 接收。图中左右图片的数据不一样,是因为我们拍照的时间不一样导致 的。 395 第三十章 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 PS2 鼠标实验 PS/2 作为电脑的标准输入接口,用于鼠标键盘等设备。PS/2 只需要一个简单的接口(2 个 IO 口),就可以外扩鼠标、键盘等,是单片机理想的输入外扩方式。 ALIENTEK MiniSTM32 开发板也自带了一个 PS/2 接口,可以用来驱动标准的鼠标、键盘 等外设,也可以用来驱动一些 PS/2 接口的小键盘,条码扫描枪等。在本章中,我们将向大家介 绍,如何在 ALIENTEK MiniSTM32 开发板上,通过 PS/2 接口来驱动电脑鼠标。本章分为如下 几个部分: 30.1 PS/2 简介 30.2 硬件设计 30.3 软件设计 30.4 下载验证 396 30.1 PS/2 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 PS/2 是电脑上常见的接口之一,用于鼠标、键盘等设备。一般情况下,PS/2 接口的鼠标为 绿色,键盘为紫色。 PS/2 接口是输入装置接口,而不是传输接口。所以 PS2 口根本没有传输速率的概念,只有 扫描速率。在 Windows 环境下,ps/2 鼠标的采样率默认为 60 次/秒,USB 鼠标的采样率为 120 次/秒。较高的采样率理论上可以提高鼠标的移动精度。 物理上的 PS/2 端口可有 2 种,一种是 5 脚的,一种是六脚的。下面给出这两种 PS/2 接口 的引脚定义图,如图 30.1.1 所示: 图 30.1.1 PS/2 引脚定义图 从图 30.1.1 可以看出,不管是 5 脚还是 6 脚的 PS/2 接头,都是有 4 根有用的线连接:时钟 线、数据线、电源线、地线。PS/2 设备的电源是 5V 的,而数据线和时钟线均是集电极开路的, 这两根信号线都需要接一个上拉电阻(开发板上使用的是 10K)。 PS/2 鼠标和键盘遵循一种双向同步串行协议,换句话说每次数据线上发送一位数据并且每 在时钟线上发一个脉冲就被读入。键盘/鼠标可以发送数据到主机,而主机也可以发送数据到设 备,但主机总是在总线上有优先权,它可以在任何时候抑制来自于键盘/鼠标的通讯,只要把时 钟拉低即可。 从设备到主机的数据在时钟信号的下降沿被主机读取,而从主机到设备的数据在时钟信号 的上升沿被设备读取。不论通信方向如何,时钟总是由设备产生的,最大的时钟频率为 33Khz, 大多数设备工作在 10~20Khz。 鼠标键盘,采用的是一种每帧包含 11/12 位的串行协议,这些位的含义如表 30.1.1 所示: 表 30.1.2 鼠标/键盘帧数据格式 表 30.1.2 中校验位的含义是:如果数据位中包含偶数个 1,则校验位为 1;如果数据位中 包含奇数个 1,则校验位为 0。数据位中的 1 的个数加上校验位总为奇数(奇校验),用于数据 侦错。当主机发送数据给键盘/鼠标的时候,设备会发送一个握手信号来应答数据已经被收到了, 该位不会出现在设备到主机的通信中。 397 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 设备到主机的通信过程: 正常情况下数据线和时钟线都是高电平,当键盘/鼠标有数据要发送时,它先检测时钟线, 确认时钟线是高电平。如果不是,则是主机抑制了通信,设备必须缓冲任何要发送的数据,直 到重新获得总线的控制权(键盘有 16 字节的缓冲区而鼠标的缓冲区仅存储最后一个要发送的 数据包)。如果时钟线是高电平,设备就可以开始传送数据了。 设备到主机的数据在时钟线的下降沿被主机读入,如图 30.1.2 所示: 图 30.1.2 设备到主机通信时序图 主机可以在设备发送数据的时候拉低时钟线来来放弃当前数据的传送。 主机到设备的通信过程: 主机到设备的通信与设备到主机的通信有点不同,因为 PS/2 的时钟总是由设备产生的,如 果主机要发送数据,则它必须首先把时钟线和数据线设置为请求发送状态。请求发送状态通过 如下过程实现: 1.拉低时钟线至少 100us 以抑制通信。 2.拉低数据线,以应用“请求发送”,然后释放时钟线。 设备在不超过 10ms 的时间内就会检测这个状态,当设备检测到这个状态后,它将开始产生 时钟信号,并且在设备提供的时钟脉冲驱动下输入八个数据位和一个停止位。主机仅当时钟线 为低的时候改变数据线,而数据在时钟脉冲的上升沿被锁存,这与发生在设备到主机通讯的过 程中正好相反。 主机到设备的通信时序图如图 30.1.3 所示: 图 30.1.3 主机到设备通信时序图 以上简单介绍了 PS/2 协议的通信过程,更多的介绍请参考《PS/2 技术参考》一文。本章 我们要驱动一个 PS/2 鼠标,所以接下来简单介绍一下 PS/2 鼠标的相关信息。 标准的 PS/2 鼠标支持下面的输入:X(左右)位移、Y(上下)位移、左键、中键和右键。 但是我们目前用到鼠标大都还有滚轮,有的还有更多的按键,这就是所谓的 Intellimouse。它支 持 5 个鼠标按键和三个位移轴(左右、上下和滚轮)。 标准的鼠标有两个计数器保持位移的跟踪:X 位移计数器和 Y 位移计数器。可存放 9 位 的 2 进制补码,并且每个计数器都有相关的溢出标志。它们的内容连同三个鼠标按钮的状态一 起以三字节移动数据包的形式发送给主机,位移计数器表示从最后一次位移数据包被送往主机 后所发生的位移量。 标准 PS/2 鼠标发送唯一和按键信息以 3 字节的数据包格式发给主机,三个数据包的意义如 图 30.1.4 所示: 398 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 30.1.4 标准鼠标位移数据包格式 位移计数器是一个 9 位 2 的补码整数,其最高位作为符号位出现在位移数据包的第一个字 节里。这些计数器在鼠标读取输入发现有位移时被更新。这些值是自从最后一次发送位移数据 包给主机后位移的累计量(即最后一次包发给主机后位移计数器被复位位移计数器可表示的值 的范围是-255 到+255)。如果超过了范围,相应的溢出位就会被置位,并在复位之前,计数器 不会再增减。 而所谓的 Intellimouse,因为多了 2 个按键和一个滚轮,所以 Intellimouse 的一个位移数据 包由 4 个字节组成,如图 30.1.5 所示: 图 30.1.5 Intellimouse 鼠标位移数据包格式 Z0-Z3 是 2 的补码,用于表示从上次数据报告以来滚轮的位移量。有效范围从-8 到+7,第 四键如果按下,则 4th Btn 位被置位,如果没有按下,则 4th Btn 位为 0。第五键也与此类似。 鼠标的介绍我们就简单的介绍到这里,详细的说明请参考光盘《PS/2 技术参考》第三章 PS/2 鼠标接口(第 36 页)。 30.2 硬件设计 本章实验功能简介:开机的时候先检测是否有鼠标接入,如果没有/检测错误,则提示错误 代码。只有在检测到 PS/2 鼠标之后才开始后续操作,当检测到鼠标之后,就在 LCD 上显示鼠 标位移数据包的内容,并转换为坐标值,在 LCD 上显示,如果有按键按下,则会提示按下的是 哪个按键。同样我们也是用 LED0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) TFTLCD 模块 3) PS/2 鼠标 本章需要用到一个PS/2接口的鼠标,大家得自备一个。下面我来看一看开发板上的PS/2 接口与STM32的连接电路,如图30.2.1所示: 图 30.2.1 PS/2 接口与 STM32 的连接电路图 可以看到,PS/2 接口与 STM32 的连接仅仅 2 个 IO 口,其中 PS_CLK 连接在 PA15 上面, 399 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 而 PS_DAT 则连接在 PC5 上面,这两个口和 KEY1 和 KEY0 复用了,所以在按键使用的时候, 就不能使用 PS/2 设备了,这个在使用的时候大家要注意一下。 30.3 软件设计 打开 PS2 鼠标实验的工程,可以看到我们在工程中新建了 ps2.c 和头文件 ps2.h 以及 mouse.c 和 mouse.h 头文件。 打开 ps2.c,代码如下: #include "ps2.h" #include "usart.h" //PS2_Status 当前状态标志 //[7]:接收到一次数据;[6]:校验错误;[5:4]:当前工作的模式;[3:0]:收到的数据长度; u8 PS2_Status=CMDMODE; //默认为命令模式 u8 PS2_DATA_BUF[16]; //ps2 数据缓存区 //位计数器 u8 BIT_Count=0; //中断 15~10 处理函数 //每 11 个 bit,为接收 1 个字节 //每接收完一个包(11 位)后,设备至少会等待 50ms 再发送下一个包 //只做了鼠标部分,键盘部分暂时未加入 EXTI_InitTypeDef EXTI_InitStructure; void EXTI15_10_IRQHandler(void) { static u8 tempdata=0; static u8 parity=0; if(EXTI_GetITStatus(EXTI_Line15)==SET) //中断 15 产生了相应的中断 { EXTI_ClearITPendingBit(EXTI_Line15); //清除 LINE15 上的中断标志位 if(BIT_Count==0) { parity=0; tempdata=0; } BIT_Count++; if(BIT_Count>1&&BIT_Count<10)//这里获得数据 { tempdata>>=1; if(PS2_SDA) { tempdata|=0x80; parity++;//记录 1 的个数 } 400 STM32 不完全手册(库函数版) }else if(BIT_Count==10)//得到校验位 ALIENTEK MiniSTM32 V3.0 开发板教程 { if(PS2_SDA)parity|=0x80;//校验位为 1 } if(BIT_Count==11)//接收到 1 个字节的数据了 { BIT_Count=parity&0x7f;//取得 1 的个数 if(((BIT_Count%2==0)&&(parity&0x80))||((BIT_Count%2==1)&&(parity&0x80)==0)) //奇偶校验 OK { //PS2_Status|=1<<7;//标记得到数据 BIT_Count=PS2_Status&0x0f; PS2_DATA_BUF[BIT_Count]=tempdata;//保存数据 if(BIT_Count<15)PS2_Status++; //数据长度加 1 BIT_Count=PS2_Status&0x30; //得到模式 switch(BIT_Count) { case CMDMODE://命令模式下,每收到一个字节都会产生接收完成 PS2_Dis_Data_Report();//禁止数据传输 PS2_Status|=1<<7; //标记得到数据 break; case KEYBOARD: break; case MOUSE: if(MOUSE_ID==0)//标准鼠标,3 个字节 { if((PS2_Status&0x0f)==3) { PS2_Status|=1<<7;//标记得到数据 PS2_Dis_Data_Report();//禁止数据传输 } }else if(MOUSE_ID==3)//扩展鼠标,4 个字节 { if((PS2_Status&0x0f)==4) { PS2_Status|=1<<7;//标记得到数据 PS2_Dis_Data_Report();//禁止数据传输 } } break; } }else 401 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { PS2_Status|=1<<6;//标记校验错误 PS2_Status&=0xf0;//清除接收数据计数器 } BIT_Count=0; } } }//禁止数据传输 //把时钟线拉低,禁止数据传输 void PS2_Dis_Data_Report(void) { PS2_Set_Int(0); //关闭中断 PS2_SET_SCL_OUT();//设置 SCL 为输出 PS2_SCL_OUT=0; //抑制传输 } //使能数据传输 //释放时钟线 void PS2_En_Data_Report(void) { PS2_SET_SCL_IN(); //设置 SCL 为输入 PS2_SET_SDA_IN(); //SDA IN PS2_SCL_OUT=1; //上拉 PS2_SDA_OUT=1; PS2_Set_Int(1); //开启中断 } //PS2 中断屏蔽设置 //en:1,开启;0,关闭; void PS2_Set_Int(u8 en) { EXTI_ClearITPendingBit(EXTI_Line15); //清除 EXTI11 线路挂起位 if(en) { EXTI_InitStructure.EXTI_LineCmd = ENABLE; }else{ EXTI_InitStructure.EXTI_LineCmd = DISABLE; } EXTI_Init(&EXTI_InitStructure); 外设 EXTI 寄存器} //等待 PS2 时钟线 sta 状态改变 //sta:1,等待变为 1;0,等待变为 0; //返回值:0,时钟线变成了 sta;1,超时溢出; //根据 EXTI_InitStruct 中指定的参数初始化 u8 Wait_PS2_Scl(u8 sta) { 402 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 u16 t=0; sta=!sta; while(PS2_SCL==sta) { t++;delay_us(1); if(t>16000)return 1;//时间溢出 (设备会在 10ms 内检测这个状态) } return 0;//被拉低了 } //在发送命令/数据之后,等待设备应带,该函数用来获取应答 //返回得到的值 //返回 0,且 PS2_Status.6=1,则产生了错误 u8 PS2_Get_Byte(void) { u16 t=0; u8 temp=0; while(1)//最大等待 55ms { t++; delay_us(10); if(PS2_Status&0x80)//得到了一次数据 { temp=PS2_DATA_BUF[PS2_Status&0x0f-1]; PS2_Status&=0x70;//清除计数器,接收到数据标记 break; }else if(t>5500||PS2_Status&0x40)break;//超时溢出/接收错误 } PS2_En_Data_Report();//使能数据传输 return temp; } //发送一个命令到 PS2. //返回值:0,无错误,其他,错误代码 u8 PS2_Send_Cmd(u8 cmd) { u8 i; u8 high=0; //记录 1 的个数 PS2_Set_Int(0); //屏蔽中断 PS2_SET_SCL_OUT(); //设置 SCL 为输出 PS2_SET_SDA_OUT(); //SDA OUT PS2_SCL_OUT=0; //拉低时钟线 delay_us(120); //保持至少 100us PS2_SDA_OUT=0; //拉低数据线 delay_us(10); PS2_SET_SCL_IN();//释放时钟线,这里 PS2 设备得到第一个位,开始位 PS2_SCL_OUT=1; 403 STM32 不完全手册(库函数版) if(Wait_PS2_Scl(0)==0)//等待时钟拉低 ALIENTEK MiniSTM32 V3.0 开发板教程 { for(i=0;i<8;i++) { if(cmd&0x01) { PS2_SDA_OUT=1; high++;} else PS2_SDA_OUT=0; cmd>>=1; Wait_PS2_Scl(1);//等待时钟拉高 发送 8 个位 Wait_PS2_Scl(0);//等待时钟拉低 } if((high%2)==0)PS2_SDA_OUT=1;//发送校验位 10 else PS2_SDA_OUT=0; Wait_PS2_Scl(1); //等待时钟拉高 10 位 Wait_PS2_Scl(0); //等待时钟拉低 PS2_SDA_OUT=1; //发送停止位 11 Wait_PS2_Scl(1); //等待时钟拉高 11 位 PS2_SET_SDA_IN(); //SDA in Wait_PS2_Scl(0); //等待时钟拉低 if(PS2_SDA==0)Wait_PS2_Scl(1); else { PS2_En_Data_Report();return 1; } }else { PS2_En_Data_Report();return 2; } PS2_En_Data_Report(); return 0; //发送成功 //等待时钟拉高 12 位 //发送失败 //发送失败 } //PS2 初始化 void PS2_Init(void) { NVIC_InitTypeDef NVIC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC, ENABLE); //使能 PC 端口时钟 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_15; //PA15 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化指定端口 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //PC5 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); //初始化指定端口 404 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource15); //GPIOC11 连中断线 11 //中断线初始化 EXTI_InitStructure.EXTI_Line=EXTI_Line15; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//中断 EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;//上升沿出发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能中断线 EXTI_Init(&EXTI_InitStructure); //初始化外设 EXTI 寄存器 //中断分组初始化 NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;//使能按键所在的中断通道 NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; //抢占优先级 1 NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; //子优先级 2 NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道 NVIC_Init(&NVIC_InitStructure); //初始化外设 NVIC 寄存器 } 该部分为底层的 PS/2 协议驱动程序,采用中断接收 PS/2 设备产生的时钟信号,然后解析。 PS2_Init 函数主要是进行一些 GPIO 初始化,中断线映射以及中断分组设置。这里相信大家已 经很熟悉这段代码了。头文件 ps2.h 中的代码很简单,相信大家都看得懂,这里不拿出来介绍 了。然后打开 mouse.c,代码如下: #include "mouse.h" #include "usart.h" #include "lcd.h" u8 MOUSE_ID;//用来标记鼠标 ID PS2_Mouse MouseX; //处理 MOUSE 的数据 void Mouse_Data_Pro(void) { MouseX.x_pos+=(signed char)PS2_DATA_BUF[1]; MouseX.y_pos+=(signed char)PS2_DATA_BUF[2]; MouseX.z_pos+=(signed char)PS2_DATA_BUF[3]; MouseX.bt_mask=PS2_DATA_BUF[0]&0X07;//取出掩码 } //初始化鼠标 //返回:0,初始化成功 //其他:错误代码 //CHECK OK 2010/5/2 u8 Init_Mouse(void) { u8 t; 405 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 PS2_Init(); delay_ms(800); PS2_Status=CMDMODE; t=PS2_Send_Cmd(PS_RESET); //等待上电复位完成 //进入命令模式 //复位鼠标 if(t!=0)return 1; t=PS2_Get_Byte(); if(t!=0XFA)return 2; t=0; while((PS2_Status&0x80)==0)//等待复位完毕 { t++; delay_ms(10); if(t>50)return 3; } PS2_Get_Byte();//得到 0XAA PS2_Get_Byte();//得到 ID 0X00 //进入滚轮模式的特殊初始化序列 PS2_Send_Cmd(SET_SAMPLE_RATE); if(PS2_Get_Byte()!=0XFA)return 4; PS2_Send_Cmd(0XC8); if(PS2_Get_Byte()!=0XFA)return 5; PS2_Send_Cmd(SET_SAMPLE_RATE); if(PS2_Get_Byte()!=0XFA)return 6; PS2_Send_Cmd(0X64); if(PS2_Get_Byte()!=0XFA)return 7; PS2_Send_Cmd(SET_SAMPLE_RATE); if(PS2_Get_Byte()!=0XFA)return 8; PS2_Send_Cmd(0X50); if(PS2_Get_Byte()!=0XFA)return 9; //序列完成 PS2_Send_Cmd(GET_DEVICE_ID); if(PS2_Get_Byte()!=0XFA)return 10; MOUSE_ID=PS2_Get_Byte(); PS2_Send_Cmd(SET_SAMPLE_RATE); if(PS2_Get_Byte()!=0XFA)return 11; PS2_Send_Cmd(0X0A); if(PS2_Get_Byte()!=0XFA)return 12; PS2_Send_Cmd(GET_DEVICE_ID); if(PS2_Get_Byte()!=0XFA)return 13; MOUSE_ID=PS2_Get_Byte(); PS2_Send_Cmd(SET_RESOLUTION); if(PS2_Get_Byte()!=0XFA)return 14; PS2_Send_Cmd(0X03); if(PS2_Get_Byte()!=0XFA)return 15; //进入设置采样率 //传输失败 //采样率 200 //传输失败 //进入设置采样率 //传输失败 //采样率 100 //传输失败 //进入设置采样率 //传输失败 //采样率 80 //传输失败 //读取 ID //传输失败 //得到 MOUSE ID //再次进入设置采样率 //传输失败 //采样率 10 //传输失败 //读取 ID //传输失败 //得到 MOUSE ID //设置分辨率 //传输失败 //8 点/mm //传输失败 406 STM32 不完全手册(库函数版) PS2_Send_Cmd(SET_SCALING11); if(PS2_Get_Byte()!=0XFA)return 16; PS2_Send_Cmd(SET_SAMPLE_RATE); if(PS2_Get_Byte()!=0XFA)return 17; ALIENTEK MiniSTM32 V3.0 开发板教程 //设置缩放比率为 1:1 //传输失败 //设置采样率 //传输失败 PS2_Send_Cmd(0X28); if(PS2_Get_Byte()!=0XFA)return 18; PS2_Send_Cmd(EN_DATA_REPORT); if(PS2_Get_Byte()!=0XFA)return 19; PS2_Status=MOUSE; return 0;//无错误,初始化成功 //40 //传输失败 //使能数据报告 //传输失败 //进入鼠标模式 } 该部分仅 2 个函数,Init_Mouse 用于初始化鼠标,让鼠标进入 Intellimouse 模式,里面的初 始化序列完全按照《PS/2 技术参考》里面介绍的来设计。另外一个函数就是将收到的数据简单 处理一下。接下来打开 mouse.h: #ifndef __MOUSE_H #define __MOUSE_H #include "ps2.h" //HOST->DEVICE 的命令集 #define PS_RESET ……//省略部分指令 //#define RESEND //鼠标结构体 0XFF //复位命令 回应 0XFA 0XFE //再次发送 typedef struct { short x_pos; //横坐标 short y_pos; //纵坐标 short z_pos; //滚轮坐标 u8 bt_mask; //按键标识,bit2 中间键;bit1,右键;bit0,左键 } PS2_Mouse; extern PS2_Mouse MouseX; extern u8 MOUSE_ID; //鼠标 ID,0X00,表示标准鼠标(3 字节);0X03 表示扩展鼠标(4 字节) u8 Init_Mouse(void); void Mouse_Data_Pro(void); #endif 该部分代码定义了一个鼠标结构体,用于存放鼠标相关的数据,并对鼠标的相关命令进行 了宏定义(部分被省略)。最后,打开 main.c 文件,代码如下: //显示鼠标的坐标值 //x,y:在 LCD 上显示的坐标位置 //pos:坐标值 void Mouse_Show_Pos(u16 x,u16 y,short pos) { if(pos<0) { 407 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_ShowChar(x,y,'-',16,0); //显示负号 pos=-pos; //转为正数 }else LCD_ShowChar(x,y,' ',16,0);//去掉负号 LCD_ShowNum(x+8,y,pos,5,16); //显示值 } int main(void) { u8 t; u8 errcnt=0; delay_init(); //延时函数初始化 NVIC_Configuration(); uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"Mouse TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/12"); while(Init_Mouse()) //检查鼠标是否在位. { LCD_ShowString(60,130,200,16,16,"Mouse Error"); delay_ms(400); LCD_Fill(60,130,239,130+16,WHITE); delay_ms(100); } LCD_ShowString(60,130,200,16,16,"Mouse OK"); LCD_ShowString(60,150,200,16,16,"Mouse ID:"); LCD_ShowNum(132,150,MOUSE_ID,3,16);//填充模式 POINT_COLOR=BLUE; LCD_ShowString(30,170,200,16,16,"BUF[0]:"); LCD_ShowString(30,186,200,16,16,"BUF[1]:"); LCD_ShowString(30,202,200,16,16,"BUF[2]:"); if(MOUSE_ID==3)LCD_ShowString(30,218,200,16,16,"BUF[3]:"); LCD_ShowString(90+30,170,200,16,16,"X POS:"); LCD_ShowString(90+30,186,200,16,16,"Y POS:"); LCD_ShowString(90+30,202,200,16,16,"Z POS:"); if(MOUSE_ID==3)LCD_ShowString(90+30,218,200,16,16,"BUTTON:"); t=0; while(1) { if(PS2_Status&0x80)//得到了一次数据 { LCD_ShowNum(56+30,170,PS2_DATA_BUF[0],3,16);//填充模式 LCD_ShowNum(56+30,186,PS2_DATA_BUF[1],3,16);//填充模式 408 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_ShowNum(56+30,202,PS2_DATA_BUF[2],3,16);//填充模式 if(MOUSE_ID==3)LCD_ShowNum(56+30,218,PS2_DATA_BUF[3],3,16); Mouse_Data_Pro(); //处理数据 Mouse_Show_Pos(146+30,170,MouseX.x_pos); //X 坐标 Mouse_Show_Pos(146+30,186,MouseX.y_pos); //Y 坐标 if(MOUSE_ID==3)Mouse_Show_Pos(146+30,202,MouseX.z_pos); //滚轮位置 if(MouseX.bt_mask&0x01)LCD_ShowString(146+30,218,200,16,16,"LEFT"); else LCD_ShowString(146+30,218,200,16,16," "); if(MouseX.bt_mask&0x02)LCD_ShowString(146+30,234,200,16,16,"RIGHT"); else LCD_ShowString(146+30,234,200,16,16," "); if(MouseX.bt_mask&0x04) LCD_ShowString(146+30,250,200,16,16,"MIDDLE"); else LCD_ShowString(146+30,250,200,16,16," "); PS2_Status=MOUSE; PS2_En_Data_Report(); //使能数据报告 }else if(PS2_Status&0x40) { errcnt++; PS2_Status=MOUSE; LCD_ShowNum(86+30,234,errcnt,3,16);//填充模式 } t++; delay_ms(1); if(t==200) { t=0; LED0=!LED0;} } } 此部分,除了 main 函数,我们还编写了 Mouse_Show_Pos 函数,用于在指定位置显示鼠标 坐标值,并支持负数显示,通过该函数,可以方便我们显示鼠标坐标数据。至此,PS/2 鼠标实 验的软件设计部分就结束了。 30.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 显示如图 30.4.1 所示内容(假定 PS/2 鼠标已经接上,并且初始化成功): 409 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 30.4.1 PS/2 鼠标实验显示结果 移动鼠标,或者按动按键,就可以看到上面的数据不断变化,证明我们的鼠标已经成功被 驱动了,接下来我们就可以使用鼠标来控制 STM32 了。 410 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三十一章 FLASH 模拟 EEPROM 实验 STM32 本身没有自带 EEPROM,但是 STM32 具有 IAP(在应用编程)功能,所以我们可 以把它的 FLASH 当成 EEPROM 来使用。本章,我们将利用 STM32 内部的 FLASH 来实现第二 十五章类似的效果,不过这次我们是将数据直接存放在 STM32 内部,而不是存放在 W25Q64。 本章分为如下几个部分: 31.1 STM32 FLASH 简介 31.2 硬件设计 31.3 软件设计 31.4 下载验证 411 31.1 STM32 FLASH 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 不同型号的 STM32,其 FLASH 容量也有所不同,最小的只有 16K 字节,最大的则达到了 1024K 字节。MiniSTM32 开发板选择的 STM32F103RCT6 的 FLASH 容量为 256K 字节,属于 大容量产品(另外还有中容量和小容量产品),大容量产品的闪存模块组织如图 31.1.1 所示: 图 31.1.1 大容量产品闪存模块组织 STM32 的闪存模块由:主存储器、信息块和闪存存储器接口寄存器等 3 部分组成。 主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。对于大容量产品,其 被划分为 256 页,每页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。从上图可以 看出主存储器的起始地址就是 0X08000000, B0、B1 都接 GND 的时候,就是从 0X08000000 开始运行代码的。 信息块,该部分分为 2 个小部分,其中启动程序代码,是用来存储 ST 自带的启动程序, 用于串口下载代码,当 B0 接 V3.3,B1 接 GND 的时候,运行的就是这部分代码。用户选择字 节,则一般用于配置写保护、读保护等功能,本章不作介绍。 闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制机构。 对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电 压由内部产生。 在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正 确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。 闪存的读取 内置闪存模块可以在通用地址空间直接寻址,任何 32 位数据的读操作都能访问闪存模块的 内容并得到相应的数据。读接口在闪存端包含一个读控制器,还包含一个 AHB 接口与 CPU 衔 接。这个接口的主要工作是产生读闪存的控制信号并预取 CPU 要求的指令块,预取指令块仅用 于在 I-Code 总线上的取指操作,数据常量是通过 D-Code 总线访问的。这两条总线的访问目标 是相同的闪存模块,访问 D-Code 将比预取指令优先级高。 这里要特别留意一个闪存等待时间,因为 CPU 运行速度比 FLASH 快得多,STM32F103 的 FLASH 最快访问速度≤24Mhz,如果 CPU 频率超过这个速度,那么必须加入等待时间,比 412 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 如我们一般使用 72Mhz 的主频,那么 FLASH 等待周期就必须设置为 2,该设置通过 FLASH_ACR 寄存器设置。 例如,我们要从地址 addr,读取一个半字(半字为 16 为,字为 32 位),可以通过如下的 语句读取: data=*(vu16*)addr; 将 addr 强制转换为 vu16 指针,然后取该指针所指向的地址的值,即得到了 addr 地址的值。 类似的,将上面的 vu16 改为 vu8,即可读取指定地址的一个字节。相对 FLASH 读取来说,STM32 FLASH 的写就复杂一点了,下面我们介绍 STM32 闪存的编程和擦除。 闪存的编程和擦除 STM32 的闪存编程是由 FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含 7 个 32 位寄存器,他们分别是:  FPEC 键寄存器(FLASH_KEYR)  选择字节键寄存器(FLASH_OPTKEYR)  闪存控制寄存器(FLASH_CR)  闪存状态寄存器(FLASH_SR)  闪存地址寄存器(FLASH_AR)  选择字节寄存器(FLASH_OBR)  写保护寄存器(FLASH_WRPR) 其中 FPEC 键寄存器总共有 3 个键值: RDPRT 键=0X000000A5 KEY1=0X45670123 KEY2=0XCDEF89AB STM32 复位后,FPEC 模块是被保护的,不能写入 FLASH_CR 寄存器;通过写入特定的序 列到 FLASH_KEYR 寄存器可以打开 FPEC 模块(即写入 KEY1 和 KEY2),只有在写保护被解 除后,我们才能操作相关寄存器。 STM32 闪存的编程每次必须写入 16 位(不能单纯的写入 8 位数据哦!),当 FLASH_CR 寄 存器的 PG 位为’1’时,在一个闪存地址写入一个半字将启动一次编程;写入任何非半字的数 据,FPEC 都会产生总线错误。在编程过程中(BSY 位为’1’),任何读写闪存的操作都会使 CPU 暂停,直到此次闪存编程结束。 同样,STM32 的 FLASH 在编程的时候,也必须要求其写入地址的 FLASH 是被擦除了的 (也就是其值必须是 0XFFFF),否则无法写入,在 FLASH_SR 寄存器的 PGERR 位将得到一个 警告。 STM23 的 FLASH 编程过程如图 31.1.2 所示: 413 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 31.1.2 STM32 闪存编程过程 从上图可以得到闪存的编程顺序如下:  检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁  检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作  设置 FLASH_CR 寄存器的 PG 位为’1’  在指定的地址写入要编程的半字  等待 BSY 位变为’0’  读出写入的地址并验证数据 前面提到,我们在 STM32 的 FLASH 编程的时候,要先判断缩写地址是否被擦除了,所以, 我们有必要再介绍一下 STM32 的闪存擦除,STM32 的闪存擦除分为两种:页擦除和整片擦除。 页擦除过程如图 31.1.3 所示 图 31.1.3 STM32 闪存页擦除过程 从上图可以看出,STM32 的页擦除顺序为:  检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁  检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的闪存操作 414 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程  设置 FLASH_CR 寄存器的 PER 位为’1’  用 FLASH_AR 寄存器选择要擦除的页  设置 FLASH_CR 寄存器的 STRT 位为’1’  等待 BSY 位变为’0’  读出被擦除的页并做验证 本章,我们只用到了 STM32 的页擦除功能,整片擦除功能我们在这里就不介绍了。通过 以上了解,我们基本上知道了 STM32 闪存的读写所要执行的步骤了,接下来,我们看看与读 写相关的寄存器说明。 第一个介绍的是 FPEC 键寄存器:FLASH_KEYR。该寄存器各位描述如图 31.1.4 所示: 图 31.1.4 寄存器 FLASH_KEYR 各位描述 该寄存器主要用来解锁 FPEC,必须在该寄存器写入特定的序列(KEY1 和 KEY2)解锁后, 才能对 FLASH_CR 寄存器进行写操作。 第二个要介绍的是闪存控制寄存器:FLASH_CR。该寄存器的各位描述如图 31.1.5 所示: 图 31.1.5 寄存器 FLASH_CR 各位描述 该寄存器我们本章只用到了它的 LOCK、STRT、PER 和 PG 等 4 个位。 LOCK 位,该位用于指示 FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后, 硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。 STRT 位,该位用于开始一次擦除操作。在该位写入 1 ,将执行一次擦除操作。 PER 位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置 1。 PG 位,该位用于选择编程操作,在往 FLASH 写数据的时候,该位需要置 1。 FLASH_CR 的其他位,我们就不在这里介绍了,请大家参考《STM32F10xxx 闪存编程参 考手册》第 18 页。 第三个要介绍的是闪存状态寄存器:FLASH_SR。该寄存器各位描述如图 31.1.6 所示: 415 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 31.1.6 寄存器 FLASH_SR 各位描述 该寄存器主要用来指示当前 FPEC 的操作编程状态。 最后,我们再来看看闪存地址寄存器:FLASH_AR。该寄存器各位描述如图 31.1.7 所示: 图 31.1.7 寄存器 FLASH_AR 各位描述 该寄存器在本章,我们主要用来设置要擦除的页。 关于 STM32 FLASH 的介绍,我们就介绍到这。更详细的介绍,请参考《STM32F10xxx 闪 存编程参考手册》。下面我们讲解使用 STM32 的官方固件库操作 FLASH 的几个常用函数。这 些函数和定义分布在文件 stm32f10x_flash.c 以及 stm32f10x_flash.h 文件中。 1. 锁定解锁函数 上面讲解到在对 FLASH 进行写操作前必须先解锁,解锁操作也就是必须在 FLASH_KEYR 寄 416 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 存器写入特定的序列(KEY1 和 KEY2),固件库函数实现很简单: void FLASH_Unlock(void); 同样的道理,在对 FLASH 写操作完成之后,我们要锁定 FLASH,使用的库函数是: void FLASH_Lock(void); 2. 写操作函数 固件库提供了三个 FLASH 写函数: FLASH_Status FLASH_ProgramWord(uint32_t Address, uint32_t Data); FLASH_Status FLASH_ProgramHalfWord(uint32_t Address, uint16_t Data); FLASH_Status FLASH_ProgramOptionByteData(uint32_t Address, uint8_t Data); 顾名思义分别为:FLASH_ProgramWord 为 32 位字写入函数,其他分别为 16 位半字写入和用 户选择字节写入函数。这里需要说明,32 位字节写入实际上是写入的两次 16 位数据,写完第 一次后地址+2,这与我们前面讲解的 STM32 闪存的编程每次必须写入 16 位并不矛盾。 3. 擦除函数 固件库提供三个 FLASH 擦除函数: FLASH_Status FLASH_ErasePage(uint32_t Page_Address); FLASH_Status FLASH_EraseAllPages(void); FLASH_Status FLASH_EraseOptionBytes(void); 这三个函数可以顾名思义了,非常简单,页擦除函数以及用户选择字节数据擦除函数。 4. 获取 FLASH 状态 主要是用的函数是: FLASH_Status FLASH_GetStatus(void); 返回值是通过枚举类型定义的: typedef enum { FLASH_BUSY = 1,//忙 FLASH_ERROR_PG,//编程错误 FLASH_ERROR_WRP,//写保护错误 FLASH_COMPLETE,//操作完成 FLASH_TIMEOUT//操作超时 }FLASH_Status; 从这里面我们可以看到 FLASH 操作的 5 个状态,每个代表的意思我们在后面注释了。 5. 等待操作完成函数 在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正 确地进行;既在进行写或擦除操作时,不能进行代码或数据的读取操作。 所以在每次操作之前,我们都要等待上一次操作完成这次操作才能开始。使用的函数是: FLASH_Status FLASH_WaitForLastOperation(uint32_t Timeout) 入口参数为等待时间,返回值是 FLASH 的状态,这个很容易理解,这个函数本身我们在固件 库中使用得不多,但是在固件库函数体中间可以多次看到。 6. 读 FLASH 特定地址数据函数 有写就必定有读,而读取 FLASH 指定地址的半字的函数固件库并没有给出来,这里我们 自己写的一个函数: u16 STMFLASH_ReadHalfWord(u32 faddr) { 417 return *(vu16*)faddr; } STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 31.2 硬件设计 本章实验功能简介:开机的时候先显示一些提示信息,然后在主循环里面检测两个按键, 其中 1 个按键(WK_UP)用来执行写入 FLASH 的操作,另外一个按键(KEY0)用来执行读 出操作,在 TFTLCD 模块上显示相关信息。同时用 DS0 提示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) WK_UP 和 KEY0 按键 3) TFTLCD 模块 4) STM32 内部 FLASH 本章需要用到的资源和电路连接,在之前已经全部有介绍过了,接下来我们直接开始软件 设计。 31.3 软件设计 打开我们的 FLASH 模拟 EEPROM 实验工程,可以看到我们添加了两个文件 stmflash.c 和 stm32flash.h 。 同 时 我 们 还 引 入 了 固 件 库 flash 操 作 文 件 stm32f10x_flash.c 和 头 文 件 stm32f10x_flash.h。 打开 stmflash.c 文件,代码如下: u16 STMFLASH_ReadHalfWord(u32 faddr) { return *(vu16*)faddr; } #if STM32_FLASH_WREN //如果使能了写 //不检查的写入 //WriteAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16 位)数 void STMFLASH_Write_NoCheck(u32 WriteAddr,u16 *pBuffer,u16 NumToWrite) { u16 i; for(i=0;i=(STM32_FLASH_BASE+1024* STM32_FLASH_SIZE)))return;//非法地址 FLASH_Unlock(); //解锁 offaddr=WriteAddr-STM32_FLASH_BASE; //实际偏移地址. secpos=offaddr/STM_SECTOR_SIZE; //扇区地址 0~127 for STM32F103RBT6 secoff=(offaddr%STM_SECTOR_SIZE)/2; //在扇区内的偏移(2 个字节为基本单位.) secremain=STM_SECTOR_SIZE/2-secoff; //扇区剩余空间大小 if(NumToWrite<=secremain)secremain=NumToWrite;//不大于该扇区范围 while(1) { STMFLASH_Read(secpos*STM_SECTOR_SIZE+STM32_FLASH_BASE,STMFLASH_BUF, STM_SECTOR_SIZE/2);//读出整个扇区的内容 for(i=0;i(STM_SECTOR_SIZE/2))secremain=STM_SECTOR_SIZE/2; //下一个扇区还是写不完 else secremain=NumToWrite;//下一个扇区可以写完了 } }; FLASH_Lock();//上锁 } #endif //从指定地址开始读出指定长度的数据 //ReadAddr:起始地址 //pBuffer:数据指针 //NumToWrite:半字(16 位)数 void STMFLASH_Read(u32 ReadAddr,u16 *pBuffer,u16 NumToRead) { u16 i; for(i=0;i=0;offset--) ALIENTEK MiniSTM32 V3.0 开发板教程 //搜索整个内存控制区 { if(!mallco_dev.memmap[offset])cmemb++; //连续空内存块数增加 else cmemb=0; //连续内存块清零 if(cmemb==nmemb) //找到了连续 nmemb 个空内存块 { for(i=0;i74 个时钟,这是因为 SD 卡内部有个供电电压上升时间,大概为 64 个 CLK,剩 下的 10 个 CLK 用于 SD 卡同步,之后才能开始 CMD0 的操作,在卡初始化的时候,CLK 时钟 最大不能超过 400Khz!。 接着我们看看 SD 卡的初始化,SD 卡的典型初始化过程如下: 1、初始化与 SD 卡连接的硬件条件(MCU 的 SPI 配置,IO 口配置); 2、上电延时(>74 个 CLK); 3、复位卡(CMD0),进入 IDLE 状态; 4、发送 CMD8,检查是否支持 2.0 协议; 5、根据不同协议检查 SD 卡(命令包括:CMD55、CMD41、CMD58 和 CMD1 等); 6、取消片选,发多 8 个 CLK,结束初始化 这样我们就完成了对 SD 卡的初始化,注意末尾发送的 8 个 CLK 是提供 SD 卡额外的时钟, 完成某些操作。通过 SD 卡初始化,我们可以知道 SD 卡的类型(V1、V2、V2HC 或者 MMC), 在完成了初始化之后,就可以开始读写数据了。 SD 卡读取数据,这里通过 CMD17 来实现,具体过程如下: 1、发送 CMD17; 2、接收卡响应 R1; 3、接收数据起始令牌 0XFE; 4、接收数据; 5、接收 2 个字节的 CRC,如果不使用 CRC,这两个字节在读取后可以丢掉。 6、禁止片选之后,发多 8 个 CLK; 以上就是一个典型的读取 SD 卡数据过程,SD 卡的写于读数据差不多,写数据通过 CMD24 来实现,具体过程如下: 1、发送 CMD24; 2、接收卡响应 R1; 3、发送写数据起始令牌 0XFE; 4、发送数据; 5、发送 2 字节的伪 CRC; 6、禁止片选之后,发多 8 个 CLK; 以上就是一个典型的写 SD 卡过程。关于 SD 卡的介绍,我们就介绍到这里,更详细的介 绍请参考光盘 SD 卡的参考资料(SD 卡 2.0 协议)。 33.2 硬件设计 本章实验功能简介:开机的时候先初始化 SD 卡,如果 SD 卡初始化完成,则提示 LCD 初 始化成功。按下 KEY0,读取 SD 卡扇区 0 的数据,然后通过串口发送到电脑。如果没初始化 437 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 通过,则在 LCD 上提示初始化失败。 同样用 DS0 来指示程序正在运行。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) SD 卡 前面四部分,在之前的实例已经介绍过了,这里我们介绍一下 MiniSTM32 开发板板载的 SD 卡接口和 STM32 的连接关系,如图 33.2.1 所示: 图33.2.1 SD卡接口与STM32连接原理图 从图中可以看出,SD卡通过4根信号线与STM32连接,SD卡的片选(SD_CS)连接PA3, SD卡的SPI接口,连接在STM32的SPI1上面,硬件连接就这么简单,这里要注意的是SPI1被3 个外设共用了:SD卡、W25Q64和NRF24L01,在使用SD卡的时候,必须禁止其他外设的片 选,以防干扰。 33.3 软件设计 打开 SD 卡实验工程,可以看到我们新建了 MMC_SD.C 文件和 MMC_SD.h,所有 SD 卡相 关的驱动代码和定义都在这两个文件中。因为 SD 卡是通过 spi 操作的,所以对应的我们会加入 stm32f10x_spi.c 源文件和对应的头文件 stm32f10x_spi.h。 打开 MMC_SD.C 文件,在该文件里面,我们输入与 SD 卡相关的操作代码,这里由于篇 幅限制,我们不贴出所有代码,仅介绍两个最重要的函数,第一个是 SD_Initialize 函数,该函 数源码如下: //初始化 SD 卡 u8 SD_Initialize(void) { u8 r1; // 存放 SD 卡的返回值 u16 retry; // 用来进行超时计数 u8 buf[4]; u16 i; SD_SPI_Init(); //初始化 IO SD_SPI_SpeedLow(); //设置到低速模式 for(i=0;i<10;i++)SD_SPI_ReadWriteByte(0XFF);//发送最少 74 个脉冲 438 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 retry=20; do { r1=SD_SendCmd(CMD0,0,0x95);//进入 IDLE 状态 }while((r1!=0X01) && retry--); SD_Type=0;//默认无卡 if(r1==0X01) { if(SD_SendCmd(CMD8,0x1AA,0x87)==1)//SD V2.0 { for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF); if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支持 2.7~3.6V { retry=0XFFFE; do { SD_SendCmd(CMD55,0,0X01); //发送 CMD55 r1=SD_SendCmd(CMD41,0x40000000,0X01);//发送 CMD41 }while(r1&&retry--); if(retry&&SD_SendCmd(CMD58,0,0X01)==0)//鉴别 SD2.0 卡版本开始 { for(i=0;i<4;i++)buf[i]=SD_SPI_ReadWriteByte(0XFF);//得到 OCR 值 if(buf[0]&0x40)SD_Type=SD_TYPE_V2HC; //检查 CCS else SD_Type=SD_TYPE_V2; } } }else//SD V1.x/ MMC V3 { SD_SendCmd(CMD55,0,0X01); //发送 CMD55 r1=SD_SendCmd(CMD41,0,0X01); //发送 CMD41 if(r1<=1) { SD_Type=SD_TYPE_V1; retry=0XFFFE; do //等待退出 IDLE 模式 { SD_SendCmd(CMD55,0,0X01); //发送 CMD55 r1=SD_SendCmd(CMD41,0,0X01);//发送 CMD41 }while(r1&&retry--); }else//MMC 卡不支持 CMD55+CMD41 识别 { SD_Type=SD_TYPE_MMC;//MMC V3 retry=0XFFFE; 439 STM32 不完全手册(库函数版) do //等待退出 IDLE 模式 ALIENTEK MiniSTM32 V3.0 开发板教程 { r1=SD_SendCmd(CMD1,0,0X01);//发送 CMD1 }while(r1&&retry--); } if(retry==0||SD_SendCmd(CMD16,512,0X01)!=0)SD_Type=SD_TYPE_ERR; } } SD_DisSelect(); //取消片选 SD_SPI_SpeedHigh(); //高速 if(SD_Type)return 0; else if(r1)return r1; return 0xaa;//其他错误 } 该函数先设置与 SD 相关的 IO 口及 SPI 初始化,然后发送 CMD0,进入 IDLE 状态,并设置 SD 卡为 SPI 模式通信,然后判断 SD 卡类型,完成 SD 卡的初始化,注意该函数调用的 SD_SPI_Init 等函数,实际是对 SPI1 的相关函数进行了一层封装,方便移植。另外一个要介绍的函数是 SD_ReadDisk,该函数用于从 SD 卡读取一个扇区的数据(这里一般为 512 字节),该函数代码如 下: //读 SD 卡 //buf:数据缓存区 //sector:扇区 //cnt:扇区数 //返回值:0,ok;其他,失败. u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt) { u8 r1; if(SD_Type!=SD_TYPE_V2HC)sector <<= 9;//转换为字节地址 if(cnt==1) { r1=SD_SendCmd(CMD17,sector,0X01);//读命令 if(r1==0) r1=SD_RecvData(buf,512);//指令发送成功,接收 512 个字节 }else { r1=SD_SendCmd(CMD18,sector,0X01);//连续读命令 do { r1=SD_RecvData(buf,512); //接收 512 个字节 buf+=512; }while(--cnt && r1==0); SD_SendCmd(CMD12,0,0X01); //发送停止命令 } SD_DisSelect();//取消片选 440 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 return r1; } 此函数根据要读取扇区的多少,发送 CMD17/CMD18 命令,然后读取一个/多个扇区的数据, 详细见代码,这里我们就不多介绍了。接下来我们打开 MMC_SD.H, 内容如下: #ifndef _MMC_SD_H_ #define _MMC_SD_H_ #include "sys.h" #include // SD 卡类型定义 #define SD_TYPE_ERR 0X00 #define SD_TYPE_MMC 0X01 #define SD_TYPE_V1 0X02 #define SD_TYPE_V2 0X04 #define SD_TYPE_V2HC // SD 卡指令表 #define CMD0 0 0X06 //卡复位 #define CMD1 1 #define CMD8 8 #define CMD9 9 #define CMD10 10 #define CMD12 12 #define CMD16 16 #define CMD17 17 #define CMD18 18 #define CMD23 23 #define CMD24 24 #define CMD25 25 #define CMD41 41 #define CMD55 55 #define CMD58 58 #define CMD59 59 //数据写入回应字意义 //命令 8 ,SEND_IF_COND //命令 9 ,读 CSD 数据 //命令 10,读 CID 数据 //命令 12,停止数据传输 //命令 16,设置 SectorSize 应返回 0x00 //命令 17,读 sector //命令 18,读 Multi sector //命令 23,设置多 sector 写入前预先擦除 N 个 block //命令 24,写 sector //命令 25,写 Multi sector //命令 41,应返回 0x00 //命令 55,应返回 0x01 //命令 58,读 OCR 信息 //命令 59,使能/禁止 CRC,应返回 0x00 #define MSD_DATA_OK 0x05 #define MSD_DATA_CRC_ERROR 0x0B #define MSD_DATA_WRITE_ERROR 0x0D #define MSD_DATA_OTHER_ERROR //SD 卡回应标记字 0xFF #define MSD_RESPONSE_NO_ERROR 0x00 #define MSD_IN_IDLE_STATE 0x01 #define MSD_ERASE_RESET 0x02 #define MSD_ILLEGAL_COMMAND 0x04 #define MSD_COM_CRC_ERROR 0x08 #define MSD_ERASE_SEQUENCE_ERROR 0x10 441 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define MSD_ADDRESS_ERROR 0x20 #define MSD_PARAMETER_ERROR 0x40 #define MSD_RESPONSE_FAILURE 0xFF //这部分应根据具体的连线来修改! //MiniSTM32 开发板使用的是 PA3 作为 SD 卡的 CS 脚. #define SD_CS PAout(3) //SD 卡片选引脚 u8 SD_SPI_ReadWriteByte(u8 data); void SD_SPI_SpeedLow(void); void SD_SPI_SpeedHigh(void); u8 SD_WaitReady(void); u8 SD_GetResponse(u8 Response); u8 SD_Initialize(void); u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt); u8 SD_WriteDisk(u8*buf,u32 sector,u8 cnt); u32 SD_GetSectorCount(void); u8 SD_GetCID(u8 *cid_data); u8 SD_GetCSD(u8 *csd_data); //等待 SD 卡准备 //获得相应 //初始化 //读块 //写块 //读扇区数 //读 SD 卡 CID //读 SD 卡 CSD #endif 该部分代码主要是一些命令的宏定义以及函数声明,在这里我们设定了 SD 卡的 CS 管脚为 PA3。最后我们来看看 main.c 的内容: //读取 SD 卡的指定扇区的内容,并通过串口 1 输出 //sec:扇区物理地址编号 void SD_Read_Sectorx(u32 sec) { u8 *buf; u16 i; buf=mymalloc(512); //申请内存 if(SD_ReadDisk(buf,sec,1)==0) //读取 0 扇区的内容 { LCD_ShowString(60,190,200,16,16,"USART1 Sending Data..."); printf("SECTOR 0 DATA:\r\n"); for(i=0;i<512;i++)printf("%x ",buf[i]);//打印 sec 扇区数据 printf("\r\nDATA ENDED\r\n"); LCD_ShowString(60,190,200,16,16,"USART1 Send Data Over!"); } myfree(buf);//释放内存 } int main(void) { u8 key; u32 sd_size; u8 t=0; delay_init(); //延时函数初始化 442 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 mem_init(); //初始化内存池 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"SD CARD TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/13"); LCD_ShowString(60,130,200,16,16,"KEY0:Read Sector 0"); while(SD_Initialize())//检测不到 SD 卡 { LCD_ShowString(60,150,200,16,16,"SD Card Error!"); delay_ms(500); LCD_ShowString(60,150,200,16,16,"Please Check! "); delay_ms(500); LED0=!LED0;//DS0 闪烁 } POINT_COLOR=BLUE;//设置字体为蓝色 //检测 SD 卡成功 LCD_ShowString(60,150,200,16,16,"SD Card OK "); LCD_ShowString(60,170,200,16,16,"SD Card Size: MB"); sd_size=SD_GetSectorCount();//得到扇区数 LCD_ShowNum(164,170,sd_size>>11,5,16);//显示 SD 卡容量 while(1) { key=KEY_Scan(0); if(key==KEY0_PRES)SD_Read_Sectorx(0);//KEY0 按,读取 SD 卡扇区 0 的内容 delay_ms(10);t++; if(t==20) { LED0=!LED0; t=0; } } } 这里总共 2 个函数,其中 SD_Read_Sectorx 用于读取 SD 卡指定扇区的数据,并将读到的 数据通过串口 1 输出。然后 main 函数则通过 SD_GetSectorCount 函数来得到 SD 卡的扇区数, 间接得到 SD 卡容量,然后在液晶上显示出来,接着我们通过按键 KEY0 控制读取 SD 卡的扇区 0, 然后把读到的数据通过串口打印出来。另外,我们对上一章学过的内存管理小试牛刀,稍微用 了下,以后我们会尽量使用内存管理来设计。 最后,我们将 SD_Read_Sectorx 函数加入 USMART 控制,这样,我们就可以通过串口调 试助手,读取 SD 卡任意一个扇区的数据,方便大家测试。 33.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 显示如图 33.4.1 所示的内容(默认 SD 卡已经接上了): 443 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 33.4.1 程序运行效果图 打开串口调试助手,按下 KEY0 就可以看到从开发板发回来的数据了,如图 33.4.2 所示: 图 33.4.2 串口收到的 SD 卡扇区 0 内容 这里请大家注意,不同的 SD 卡,读出来的扇区 0 是不尽相同的,所以不要因为你读出来 的数据和图 33.4.2 不同而感到惊讶。 444 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三十四章 FATFS 实验 上一章,我们学习了 SD 卡的使用,不过仅仅是简单的实现读扇区而已,真正要好好应用 SD 卡,必须使用文件系统管理,本章,我们将使用 FATFS 来管理 SD 卡,实现 SD 卡文件的读 写等基本功能。本章分为如下几个部分: 34.1 FATFS 简介 34.2 硬件设计 34.3 软件设计 34.4 下载验证 445 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 34.1 FATFS 简介 FATFS 是一个完全免费开源的 FAT 文件系统模块,专门为小型的嵌入式系统而设计。它完 全用标准 C 语言编写,所以具有良好的硬件平台独立性,可以移植到 8051、PIC、AVR、SH、 Z80、H8、ARM 等系列单片机上而只需做简单的修改。它支持 FATl2、FATl6 和 FAT32,支持 多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对 8 位单片机和 16 位 单片机做了优化。 FATFS 的特点有:  Windows 兼容的 FAT 文件系统(支持 FAT12/FAT16/FAT32)  与平台无关,移植简单  代码量少、效率高  多种配置选项  支持多卷(物理驱动器或分区,最多 10 个卷)  多个 ANSI/OEM 代码页包括 DBCS  支持长文件名、ANSI/OEM 或 Unicode  支持 RTOS  支持多种扇区大小  只读、最小化的 API 和 I/O 缓冲区等 FATFS 的这些特点,加上免费、开源的原则,使得 FATFS 应用非常广泛。FATFS 模块的层 次结构如图 34.1.1 所示: 应用层 FATFS模块 底层存储媒介接口 (SD卡/ATA/USB/NAND) RTC 图 34.1.1 FATFS 层次结构图 最顶层是应用层,使用者无需理会 FATFS 的内部结构和复杂的 FAT 协议,只需要调用 FATFS 模块提供给用户的一系列应用接口函数,如 f_open,f_read,f_write 和 f_close 等,就可 以像在 PC 上读/写文件那样简单。 中间层 FATFS 模块,实现了 FAT 文件读/写协议。FATFS 模块提供的是 ff.c 和 ff.h。除非 有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。 需要我们编写移植代码的是 FATFS 模块提供的底层接口,它包括存储媒介读/写接口(disk I/O)和供给文件创建修改时间的实时时钟。 FATFS 的源码,大家可以在:http://elm-chan.org/fsw/ff/00index_e.html 这个网站下载到,目 前最新版本为 R0.10a。本章我们就使用最新版本的 FATFS 作为介绍,下载最新版本的 FATFS 软件包,解压后可以得到两个文件夹:doc 和 src。doc 里面主要是对 FATFS 的介绍,而 src 里 面才是我们需要的源码。 446 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 其中,与平台无关的是: ffconf.h FATFS 模块配置文件 ff.h FATFS 和应用模块公用的包含文件 ff.c FATFS 模块 diskio.h FATFS 和 disk I/O 模块公用的包含文件 interger.h 数据类型定义 option 可选的外部功能(比如支持中文等) 与平台相关的代码(需要用户提供)是: diskio.c FATFS 和 disk I/O 模块接口层文件 FATFS 模块在移植的时候,我们一般只需要修改 2 个文件,即 ffconf.h 和 diskio.c。FATFS 模块的所有配置项都是存放在 ffconf.h 里面,我们可以通过配置里面的一些选项,来满足自己 的需求。接下来我们介绍几个重要的配置选项。 1)_FS_TINY。这个选项在 R0.07 版本中开始出现,之前的版本都是以独立的 C 文件出现 (FATFS 和 Tiny FATFS),有了这个选项之后,两者整合在一起了,使用起来更方便。我们使 用 FATFS,所以把这个选项定义为 0 即可。 2)_FS_READONLY。这个用来配置是不是只读,本章我们需要读写都用,所以这里设置 为 0 即可。 3)_USE_STRFUNC。这个用来设置是否支持字符串类操作,比如 f_putc,f_puts 等,本章 我们需要用到,故设置这里为 1。 4)_USE_MKFS。这个用来定时是否使能格式化,本章需要用到,所以设置这里为 1。 5)_USE_FASTSEEK。这个用来使能快速定位,我们设置为 1,使能快速定位。 6)_USE_LABEL。这个用来设置是否支持磁盘盘符(磁盘名字)读取与设置。我们设置 为 1,使能,就可以通过相关函数读取或者设置磁盘的名字了。 7)_CODE_PAGE。这个用于设置语言类型,包括很多选项(见 FATFS 官网说明),我们 这里设置为 936,即简体中文(GBK 码,需要 c936.c 文件支持,该文件在 option 文件夹)。 8)_USE_LFN。该选项用于设置是否支持长文件名(还需要_CODE_PAGE 支持),取值范 围为 0~3。0,表示不支持长文件名,1~3 是支持长文件名,但是存储地方不一样,我们选择使 用 3,通过 ff_memalloc 函数来动态分配长文件名的存储区域。 9)_VOLUMES。用于设置 FATFS 支持的逻辑设备数目,我们设置为 2,即支持 2 个设备。 10)_MAX_SS。扇区缓冲的最大值,一般设置为 512。 其他配置项,我们这里就不一一介绍了,FATFS 的说明文档里面有很详细的介绍,大家自 己阅读即可。下面我们来讲讲 FATFS 的移植,FATFS 的移植主要分为 3 步: ① 数据类型:在 integer.h 里面去定义好数据的类型。这里需要了解你用的编译器的数 据类型,并根据编译器定义好数据类型。 ② 配置:通过 ffconf.h 配置 FATFS 的相关功能,以满足你的需要。 ③ 函数编写:打开 diskio.c,进行底层驱动编写,一般需要编写 6 个接口函数,如 图 34.1.2 所示: 447 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 34.1.2 diskio 需要实现的函数 通过以上三步,我们即可完成对 FATFS 的移植。 第一步,我们使用的是 MDK5 编译器,器数据类型和 integer.h 里面定义的一致,所以此步, 我们不需要做任何改动。 第二步,关于 ffconf.h 里面的相关配置,我们在前面已经有介绍(之前介绍的 10 个配置), 我们将对应配置修改为我们介绍时候的值即可,其他的配置用默认配置。 第三步,因为 FATFS 模块完全与磁盘 I/O 层分开,因此需要下面的函数来实现底层物理磁 盘的读写与获取当前时间。底层磁盘 I/O 模块并不是 FATFS 的一部分,并且必须由用户提供。 这些函数一般有 6 个,在 diskio.c 里面。 首先是 disk_initialize 函数,该函数介绍如图 34.1.3 所示: 图 34.1.3 disk_initialize 函数介绍 第二个函数是 disk_status 函数,该函数介绍如图 34.1.4 所示: 图 34.1.4 disk_status 函数介绍 第三个函数是 disk_read 函数,该函数介绍如图 34.1.5 所示: 448 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 34.1.5 disk_read 函数介绍 第四个函数是 disk_write 函数,该函数介绍如图 34.1.6 所示: 图 34.1.6 disk_write 函数介绍 第五个函数是 disk_ioctl 函数,该函数介绍如图 34.1.7 所示: 449 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 34.1.7 disk_ioctl 函数介绍 最后一个函数是 get_fattime 函数,该函数介绍如图 34.1.8 所示: 图 34.1.8 get_fattime 函数介绍 以上六个函数,我们将在软件设计部分一一实现。通过以上 3 个步骤,我们就完成了对 FATFS 的移植,就可以在我们的代码里面使用 FATFS 了。 FATFS 提供了很多 API 函数,这些函数 FATFS 的自带介绍文件里面都有详细的介绍(包括 参考代码),我们这里就不多说了。这里需要注意的是,在使用 FATFS 的时候,必须先通过 f_mount 函数注册一个工作区,才能开始后续 API 的使用,关于 FATFS 的介绍,我们就介绍到这里。大 家可以通过 FATFS 自带的介绍文件进一步了解和熟悉 FATFS 的使用。 450 34.2 硬件设计 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 本章实验功能简介:开机的时候先初始化 SD 卡,初始化成功之后,注册两个工作区(一 个给 SD 卡用,一个给 SPI FLASH 用),然后获取 SD 卡的容量和剩余空间,并显示在 LCD 模块上,最后等待 USMART 输入指令进行各项测试。本实验通过 DS0 指示程序运行状态。 本实验用到的硬件资源有: 1) 指示灯 DS0 2) 串口 3) TFTLCD 模块 4) SD 卡 5) SPI FLASH 这些,我们在之前都已经介绍过,如有不清楚,请参考之前内容。 34.3 软件设计 本章,我们将 FATFS 部分单独做一个分组,在工程目录下新建一个 FATFS 的文件夹,然 后将 FATFS R0.10a 程序包解压到该文件夹下。同时,我们在 FATFS 文件夹里面新建一个 exfuns 的文件夹,用于存放我们针对 FATFS 做的一些扩展代码。设计完如图 34.3.1 所示: 图 34.3.1 FATFS 文件夹子目录 打开实验工程,由于本章还需要用到 W25Q64,所以,我们添加 flash.c 这个文件,并修改 SPI_FLASH_BUFFER 数组的实现方式,增加动态内存管理方式,详见本例程 flash.c 文件。 然后,我们还新建一个 FATFS 分组,然后将图 34.3.1 的 src 文件夹里面的 ff.c、diskio.c 以 及 option 文件夹下的 cc936.c 等 3 个文件加入到 FATFS 组下,并将 src 文件夹加入头文件包含 路径。 打开 diskio.c,代码如下: #include "mmc_sd.h" #include "diskio.h" #include "flash.h" #include "malloc.h" #define SD_CARD 0 //SD 卡,卷标为 0 #define EX_FLASH 1 //外部 flash,卷标为 1 #define FLASH_SECTOR_SIZE 512 451 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 //对于 W25Q64 //前 4.8M 字节给 fatfs 用,4.8M 字节后~4.8M+100K 给用户用,4.9M 以后,用于存放字库 u16 FLASH_SECTOR_COUNT= 9832; //4.8M 字节,默认为 W25Q64 #define FLASH_BLOCK_SIZE 8 //每个 BLOCK 有 8 个扇区 //初始化磁盘 DSTATUS disk_initialize (BYTE pdrv) { u8 res=0; switch(pdrv) { case SD_CARD://SD 卡 res = SD_Initialize();//SD_Initialize() if(res)//sd 卡操作失败的时候如果不执行下面的语句,可能导致 SPI 读写异常 { SD_SPI_SpeedLow(); SD_SPI_ReadWriteByte(0xff);//提供额外的 8 个时钟 SD_SPI_SpeedHigh(); } break; case EX_FLASH://外部 flash W25Q64 SPI_Flash_Init(); if(SPI_FLASH_TYPE==W25Q64)FLASH_SECTOR_COUNT=9832; else FLASH_SECTOR_COUNT=0; break; default: res=1; } if(res)return STA_NOINIT; else return 0; //初始化成功 } //获得磁盘状态 DSTATUS disk_status (BYTE pdrv) { return 0; } //读扇区 //drv:磁盘编号 0~9 //*buff:数据接收缓冲首地址 //sector:扇区地址 //count:需要读取的扇区数 DRESULT disk_read ( BYTE pdrv, /* Physical drive nmuber (0..) */ BYTE *buff, /* Data buffer to store read data */ DWORD sector, /* Sector address (LBA) */ 452 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 UINT count /* Number of sectors to read (1..128) */ ) { u8 res=0; if (!count)return RES_PARERR;//count 不能等于 0,否则返回参数错误 switch(pdrv) { case SD_CARD://SD 卡 res=SD_ReadDisk(buff,sector,count); if(res)//sd 卡操作失败的时候如果不执行下面的语句,可能导致 SPI 读写异常 { SD_SPI_SpeedLow(); SD_SPI_ReadWriteByte(0xff);//提供额外的 8 个时钟 SD_SPI_SpeedHigh(); } break; case EX_FLASH://外部 flash for(;count>0;count--) { SPI_Flash_Read(buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE); sector++; buff+=FLASH_SECTOR_SIZE; } res=0; break; default: res=1; } //处理返回值,将 SPI_SD_driver.c 的返回值转成 ff.c 的返回值 if(res==0x00)return RES_OK; else return RES_ERROR; } //写扇区 //drv:磁盘编号 0~9 //*buff:发送数据首地址 //sector:扇区地址 //count:需要写入的扇区数 #if _USE_WRITE DRESULT disk_write ( BYTE pdrv, /* Physical drive nmuber (0..) */ const BYTE *buff, /* Data to be written */ DWORD sector, /* Sector address (LBA) */ UINT count /* Number of sectors to write (1..128) */ 453 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 ) { u8 res=0; if (!count)return RES_PARERR;//count 不能等于 0,否则返回参数错误 switch(pdrv) { case SD_CARD://SD 卡 res=SD_WriteDisk((u8*)buff,sector,count); break; case EX_FLASH://外部 flash for(;count>0;count--) { SPI_Flash_Write((u8*)buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE); sector++; buff+=FLASH_SECTOR_SIZE; } res=0; break; default: res=1; } //处理返回值,将 SPI_SD_driver.c 的返回值转成 ff.c 的返回值 if(res == 0x00)return RES_OK; else return RES_ERROR; } #endif //其他表参数的获得 //drv:磁盘编号 0~9 //ctrl:控制代码 //*buff:发送/接收缓冲区指针 #if _USE_IOCTL DRESULT disk_ioctl ( BYTE pdrv, /* Physical drive nmuber (0..) */ BYTE cmd, /* Control code */ void *buff /* Buffer to send/receive control data */ ) { DRESULT res; if(pdrv==SD_CARD)//SD 卡 { switch(cmd) { case CTRL_SYNC: SD_CS=0; 454 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 if(SD_WaitReady()==0)res = RES_OK; else res = RES_ERROR; SD_CS=1; break; case GET_SECTOR_SIZE: *(WORD*)buff = 512; res = RES_OK; break; case GET_BLOCK_SIZE: *(WORD*)buff = 8; res = RES_OK; break; case GET_SECTOR_COUNT: *(DWORD*)buff = SD_GetSectorCount(); res = RES_OK; break; default: res = RES_PARERR; break; } }else if(pdrv==EX_FLASH) //外部 FLASH { switch(cmd) { case CTRL_SYNC: res = RES_OK; break; case GET_SECTOR_SIZE: *(WORD*)buff = FLASH_SECTOR_SIZE; res = RES_OK; break; case GET_BLOCK_SIZE: *(WORD*)buff = FLASH_BLOCK_SIZE; res = RES_OK; break; case GET_SECTOR_COUNT: *(DWORD*)buff = FLASH_SECTOR_COUNT; res = RES_OK; break; default: res = RES_PARERR; break; } 455 STM32 不完全手册(库函数版) }else res=RES_ERROR;//其他的不支持 ALIENTEK MiniSTM32 V3.0 开发板教程 return res; } #endif //获得时间 //User defined function to give a current time to fatfs module */ //31-25: Year(0-127 org.1980), 24-21: Month(1-12), 20-16: Day(1-31) */ //15-11: Hour(0-23), 10-5: Minute(0-59), 4-0: Second(0-29 *2) */ DWORD get_fattime (void) { return 0; } //动态分配内存 void *ff_memalloc (UINT size) { return (void*)mymalloc(size); } //释放内存 void ff_memfree (void* mf) { myfree(mf); } 该部分代码实现了我们 34.1 节提到的 6 个函数,同时因为在 ffconf.h 里面设置对长文件 名的支持为方法 3,所以必须实现 ff_memalloc 和 ff_memfree 这两个函数。本章,我们用 FATFS 管理了 2 个磁盘:SD 卡和 SPI FLASH。SD 卡比较好说,但是 SPI FLASH,因为其扇区是 4K 字节 大小,我们为了方便设计,强制将其扇区定义为 512 字节,这样带来的好处就是设计使用相对 简单,坏处就是擦除次数大增,所以不要随便往 SPI FLASH 里面写数据,非必要最好别写,如 果频繁写的话,很容易将 SPI FLASH 写坏。 保存 diskio.c,然后打开 ffconf.h,修改相关配置,并保存,此部分就不贴代码了,请大 家参考光盘源码。 前面提到,我们在 FATFS 文件夹下还新建了一个 exfuns 的文件夹,该文件夹用于保存一些 FATFS 一些针对 FATFS 的扩展代码,本章,我们编写了 4 个文件,分别是:exfuns.c、exfuns.h、 fattester.c 和 fattester.h。其中 exfuns.c 主要定义了一些全局变量,方便 FATFS 的使用, 同时实现了磁盘容量获取等函数。而 fattester.c 文件则主要是为了测试 FATFS 用,因为 FATFS 的很多函数无法直接通过 USMART 调用,所以我们在 fattester.c 里面对这些函数进行了一次再 封装,使其可以通过 USMART 调用。这些代码,我们就不贴出来了,请大家参考光盘源码,我们 将 exfuns.c 和 fattester.c 加入 FATFS 组下,同时将 exfuns 文件夹加入头文件包含路径。 然后,我们打开 main.c, main 函数如下: int main(void) { u32 total,free; u8 t=0; delay_init(); //延时函数初始化 456 STM32 不完全手册(库函数版) uart_init(9600); exfuns_init(); LCD_Init(); LED_Init(); ALIENTEK MiniSTM32 V3.0 开发板教程 //串口初始化为 9600 //为 fatfs 相关变量申请内存 //初始化液晶 //LED 初始化 usmart_dev.init(72); mem_init(); //初始化内存池 POINT_COLOR=RED;//设置字体为红色 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"FATFS TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"Use USMART for test"); LCD_ShowString(60,130,200,16,16,"2014/3/14"); while(SD_Initialize()) //检测 SD 卡 { LCD_ShowString(60,150,200,16,16,"SD Card Error!"); delay_ms(200); LCD_Fill(60,150,240,150+16,WHITE); delay_ms(200);//清除显示 LED0=!LED0;//DS0 闪烁 } exfuns_init(); f_mount(fs[0],"0:",1); f_mount(fs[1],"1:",1); while(exf_getfree("0",&total,&free)) //为 fatfs 相关变量申请内存 //挂载 SD 卡 //挂载 FLASH. //得到 SD 卡的总容量和剩余容量 { LCD_ShowString(60,150,200,16,16,"Fatfs Error!"); delay_ms(200); LCD_Fill(60,150,240,150+16,WHITE); delay_ms(200);//清除显示 LED0=!LED0;//DS0 闪烁 } POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(60,150,200,16,16,"FATFS OK!"); LCD_ShowString(60,170,200,16,16,"SD Total Size: MB"); LCD_ShowString(60,190,200,16,16,"SD Free Size: MB"); LCD_ShowNum(172,170,total>>10,5,16); //显示 SD 卡总容量 MB LCD_ShowNum(172,190,free>>10,5,16); //显示 SD 卡剩余容量 MB while(1) { t++; delay_ms(200); LED0=!LED0; } } 在 main 函数里面,我们为 SD 卡和 FLASH 都注册了工作区(挂载),在初始化 SD 卡并显示 其容量信息后,进入死循环,等待 USMART 测试。 最后,我们在 usmart_config.c 里面的 usmart_nametab 数组添加如下内容: (void*)mf_mount,"u8 mf_mount(u8* path,u8 mt)", 457 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 (void*)mf_open,"u8 mf_open(u8*path,u8 mode)", (void*)mf_close,"u8 mf_close(void)", (void*)mf_read,"u8 mf_read(u16 len)", (void*)mf_write,"u8 mf_write(u8*dat,u16 len)", (void*)mf_opendir,"u8 mf_opendir(u8* path)", (void*)mf_closedir,"u8 mf_closedir(void)", (void*)mf_readdir,"u8 mf_readdir(void)", (void*)mf_scan_files,"u8 mf_scan_files(u8 * path)", (void*)mf_showfree,"u32 mf_showfree(u8 *drv)", (void*)mf_lseek,"u8 mf_lseek(u32 offset)", (void*)mf_tell,"u32 mf_tell(void)", (void*)mf_size,"u32 mf_size(void)", (void*)mf_mkdir,"u8 mf_mkdir(u8*pname)", (void*)mf_fmkfs,"u8 mf_fmkfs(u8* path,u8 mode,u16 au)", (void*)mf_unlink,"u8 mf_unlink(u8 *pname)", (void*)mf_rename,"u8 mf_rename(u8 *oldname,u8* newname)", (void*)mf_getlabel,"void mf_getlabel(u8 *path)", (void*)mf_setlabel,"void mf_setlabel(u8 *path)", (void*)mf_gets,"void mf_gets(u16 size)", (void*)mf_putc,"u8 mf_putc(u8 c)", (void*)mf_puts,"u8 mf_puts(u8*c)", 这些函数均是在 fattester.c 里面实现,通过调用这些函数,即可实现对 FATFS 对应 API 函数的测试。 至此,软件设计部分就结束了。 34.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 显示如图 34.4.1 所示的内容(默认 SD 卡已经接上了): 458 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 34.4.1 程序运行效果图 打开串口调试助手,我们就可以串口调用前面添加的各种 FATFS 测试函数了,比如我们输 入 mf_scan_files("0:"),即可扫描 SD 卡根目录的所有文件,如图 34.4.2 所示: 图 34.4.2 扫描 SD 卡根目录所有文件 其他函数的测试,用类似的办法即可实现。注意这里 0 代表 SD 卡,1 代表 SPI FLASH (W25Q64)。另外,提醒大家,mf_unlink 函数,在删除文件夹的时候,必须保证文件夹是空 的,才可以正常删除,否则不能删除。 459 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三十五章 汉字显示实验 汉字显示在很多单片机系统都需要用到,少则几个字,多则整个汉字库的支持,更有甚者 还要支持多国字库,那就更麻烦了。本章,我们将向大家介绍,如何用 STM32 控制 LCD 显示 汉字。在本章中,我们将使用外部 FLASH 来存储字库,并可以通过 SD 卡更新字库。STM32 读取存在 FLASH 里面的字库,然后将汉字显示在 LCD 上面。本章分为如下几个部分: 35.1 汉字显示原理简介 35.2 硬件设计 35.3 软件设计 35.4 下载验证 460 35.1 汉字显示原理简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 常用的汉字内码系统有 GB2312,GB13000,GBK,BIG5(繁体)等几种,其中 GB2312 支持的汉字仅有几千个,很多时候不够用,而 GBK 内码不仅完全兼容 GB2312,还支持了繁体 字,总汉字数有 2 万多个,完全能满足我们一般应用的要求。 本实例我们将制作三个 GBK 字库,制作好的字库放在 SD 卡里面,然后通过 SD 卡,将字 库文件复制到外部 FLASH 芯片 W25Q64 里,这样,W25Q64 就相当于一个汉字字库芯片了。 汉字在液晶上的显示原理与前面显示字符的是一样的。汉字在液晶上的显示其实就是一些 点的显示与不显示,这就相当于我们的笔一样,有笔经过的地方就画出来,没经过的地方就不 画。所以要显示汉字,我们首先要知道汉字的点阵数据,这些数据可以由专门的软件来生成。 只要知道了一个汉字点阵的生成方法,那么我们在程序里面就可以把这个点阵数据解析成一个 汉字。 知道显示了一个汉字,就可以推及整个汉字库了。汉字在各种文件里面的存储不是以点阵 数据的形式存储的(否则那占用的空间就太大了),而是以内码的形式存储的,就是 GB2312/GBK/BIG5 等这几种的一种,每个汉字对应着一个内码,在知道了内码之后再去字库 里面查找这个汉字的点阵数据,然后在液晶上显示出来。这个过程我们是看不到,但是计算机 是要去执行的。 单片机要显示汉字也与此类似:汉字内码(GBK/GB2312)查找点阵库解析显示。 所以只要我们有了整个汉字库的点阵,就可以把电脑上的文本信息在单片机上显示出来了。 这里我们要解决的最大问题就是制作一个与汉字内码对得上号的汉字点阵库。而且要方便单片 机的查找。每个 GBK 码由 2 个字节组成,第一个字节为 0X81~0XFE,第二个字节分为两部分, 一是 0X40~0X7E,二是 0X80~0XFE。其中与 GB2312 相同的区域,字完全相同。 我们把第一个字节代表的意义称为区,那么 GBK 里面总共有 126 个区(0XFE-0X81+1), 每个区内有 190 个汉字(0XFE-0X80+0X7E-0X40+2),总共就有 126*190=23940 个汉字。我 们的点阵库只要按照这个编码规则从 0X8140 开始,逐一建立,每个区的点阵大小为每个汉字 所用的字节数*190。这样,我们就可以得到在这个字库里面定位汉字的方法: 当 GBKL<0X7F 时:Hp=((GBKH-0x81)*190+GBKL-0X40)*csize; 当 GBKL>0X80 时:Hp=((GBKH-0x81)*190+GBKL-0X41)*csize; 其中 GBKH、GBKL 分别代表 GBK 的第一个字节和第二个字节(也就是高位和低位),Hp 为对应汉字点阵数据在字库里面的起始地址(假设是从 0 开始存放),csize 代表一个汉字点阵所 占的字节数。假定采用与 15.3 节 ASCII 字库一样的提取方法(从上到下,从左到右),可以得 出字体大小与点阵所占字节数的对应关系为: csize=(size/8+((size%8)?1:0))*size; size 为字体大小,比如 12(12*12)、16(16*16)、24(24*24)等。 这样我们只要得到了汉字的 GBK 码,就可以得到该汉字点阵在点阵库里面的位置,从而 获取其点阵数据,显示这个汉字了。 上一章,我们提到要用 cc936.c,以支持长文件名,但是 cc936.c 文件里面的两个数组太大 了(172KB),直接刷在单片机里面,太占用 flash 了,所以我们必须把这两个数组存放在外部 flash。cc936 里面包含的两个数组 oem2uni 和 uni2oem 存放 unicode 和 gbk 的互相转换对照表, 这两个数组很大,这里我们利用 ALIENTEK 提供的一个 C 语言数组转 BIN(二进制)的软件: C2B 转换助手 V1.1.exe,将这两个数组转为 BIN 文件,我们将这两个数组拷贝出来存放为一个 新的文本文件,假设为 UNIGBK.TXT,然后用 C2B 转换助手打开这个文本文件,如图 35.1.1 所示: 461 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 35.1.1 C2B 转换助手 然后点击转换,就可以在当前目录下(文本文件所在目录下)得到一个 UNIGBK.bin 的文 件。这样就完成将 C 语言数组转换为.bin 文件,然后只需要将 UNIGBK.bin 保存到外部 FLASH 就实现了该数组的转移。 在 cc936.c 里面,主要是通过 ff_convert 调用这两个数组,实现 UNICODE 和 GBK 的互转, 该函数原代码如下: WCHAR ff_convert ( /* Converted code, 0 means conversion error */ WCHAR src, /* Character code to be converted */ UINT dir /* 0: Unicode to OEMCP, 1: OEMCP to Unicode */ ) { const WCHAR *p; WCHAR c; int i, n, li, hi; if (src < 0x80) { /* ASCII */ c = src; } else { if (dir) { /* OEMCP to unicode */ p = oem2uni; hi = sizeof(oem2uni) / 4 - 1; } else { /* Unicode to OEMCP */ p = uni2oem; hi = sizeof(uni2oem) / 4 - 1; } li = 0; for (n = 16; n; n--) { i = li + (hi - li) / 2; if (src == p[i * 2]) break; 462 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 if (src > p[i * 2]) li = i; else hi = i; } c = n ? p[i * 2 + 1] : 0; } return c; } 此段代码,通过二分法(16 阶)在数组里面查找 UNICODE(或 GBK)码对应的 GBK(或 UNICODE)码。当我们将数组存放在外部 flash 的时候,将该函数修改为: WCHAR ff_convert ( /* Converted code, 0 means conversion error */ WCHAR src, /* Character code to be converted */ UINT dir /* 0: Unicode to OEMCP, 1: OEMCP to Unicode */ ) { WCHAR t[2]; WCHAR c; u32 i, li, hi; u16 n; u32 gbk2uni_offset=0; if (src < 0x80)c = src;//ASCII,直接不用转换. else { if(dir) gbk2uni_offset=ftinfo.ugbksize/2; //GBK 2 UNICODE else gbk2uni_offset=0; //UNICODE 2 GBK /* Unicode to OEMCP */ hi=ftinfo.ugbksize/2;//对半开. hi =hi / 4 - 1; li = 0; for (n = 16; n; n--) { i = li + (hi - li) / 2; SPI_Flash_Read((u8*)&t,ftinfo.ugbkaddr+i*4+gbk2uni_offset,4);//读出 4 个字节 if (src == t[0]) break; if (src > t[0])li = i; else hi = i; } c = n ? t[1] : 0; } return c; } 代码中的 ftinfo.ugbksize 为我们刚刚生成的 UNIGBK.bin 的大小,而 ftinfo.ugbkaddr 是我们 存放 UNIGBK.bin 文件的首地址。这里同样采用的是二分法查找,关于 cc936.c 的修改,我们就 介绍到这。 463 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 字库的生成,我们要用到一款软件,由易木雨软件工作室设计的点阵字库生成器 V3.8。该 软件可以在 WINDOWS 系统下生成任意点阵大小的 ASCII,GB2312(简体中文)、GBK(简体中 文)、BIG5(繁体中文)、HANGUL(韩文)、SJIS(日文)、Unicode 以及泰文,越南文、俄文、乌克 兰文,拉丁文,8859 系列等共二十几种编码的字库,不但支持生成二进制文件格式的文件,也 可以生成 BDF 文件,还支持生成图片功能,并支持横向,纵向等多种扫描方式,且扫描方式 可以根据用户的需求进行增加。该软件的界面如图 35.1.1 所示: 图 35.1.2 点阵字库生成器默认界面 本章,我们总共要生成 3 个字库:12*12 字库、16*16 字库和 24*24 字库。这里以 16*16 字库为例进行介绍,其他两个字库的制作方法类似。 要生成 16*16 的 GBK 字库,则选择:936 中文 PRC GBK,字宽和高均选择 16,字体大小 选择 12,然后模式选择纵向取模方式二(字节高位在前,低位在后),最后点击创建,就可以 开始生成我们需要的字库了(.DZK 文件)。具体设置如图 35.1.3 所示: 图 35.1.3 生成 GBK16*16 字库的设置方法 注意:电脑端的字体大小与我们生成点阵大小的关系为: fsize=dsize*6/8 其中,fsize 是电脑端字体大小,dsize 是点阵大小(12、16、24 等)。所以 16*16 点阵大小 464 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 对应的是 12 字体。 生成完以后,我们把文件名和后缀改成:GBK16.FON。同样的方法,生成 12*12 的点阵库 (GBK12.FON)和 24*24 的点阵库(GBK24.FON),总共制作 3 个字库。 另外,该软件还可以生成其他很多字库,字体也可选,大家可以根据自己的需要按照上面 的方法生成即可。该软件的详细介绍请看软件自带的《点阵字库生成器说明书》,关于汉字显示 原理,我们就介绍到这。 35.2 硬件设计 本章实验功能简介:开机的时候先检测 W25Q64 中是否已经存在字库,如果存在,则按次 序显示汉字(两种字体都显示)。如果没有,则检测 SD 卡和文件系统,并查找 SYSTEM 文件夹 下 的 FONT 文 件 夹 , 在 该 文 件 夹 内 查 找 UNIGBK.BIN 、 GBK12.FON 、 GBK16.FON 和 GBK24.FON(这几个文件的由来,我们前面已经介绍了)。在检测到这些文件之后,就开始 更新字库,更新完毕才开始显示汉字。通过按按键 KEY0,可以强制更新字库。同样我们也是 用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 按键 3) 串口 4) TFTLCD 模块 5) SD 卡 6) SPI FLASH 这几部分分,在之前的实例中都介绍过了,我们在此就不介绍了。 35.3 软件设计 打开汉字显示实验工程可以看到,首先我们在 HARDWARE 文件夹所在的文件夹下新建一 个 TEXT 的文件夹。在 TEXT 文件夹下新建 fontupd.c、fontupd.h、text.c、text.h 这 4 个文件。 并将该文件夹加入头文件包含路径。 打开 fontupd.c,在该文件内代码如下: #include "fontupd.h" #include "ff.h" #include "flash.h" #include "lcd.h" #include "malloc.h" //字库存放起始地址 #define FONTINFOADDR (4916+100)*1024 //MiniSTM32 是从 4.8M+100K 地址开始的 //字库信息结构体. //用来保存字库基本信息,地址,大小等 _font_info ftinfo; //字库存放在 sd 卡中的路径 const u8 *GBK24_PATH="0:/SYSTEM/FONT/GBK24.FON"; //GBK24 的存放位置 const u8 *GBK16_PATH="0:/SYSTEM/FONT/GBK16.FON"; //GBK16 的存放位置 const u8 *GBK12_PATH="0:/SYSTEM/FONT/GBK12.FON"; //GBK12 的存放位置 const u8 *UNIGBK_PATH="0:/SYSTEM/FONT/UNIGBK.BIN";//UNIGBK.BIN 的存放位置 465 STM32 不完全手册(库函数版) //显示当前字体更新进度 //x,y:坐标 //size:字体大小 //fsize:整个文件大小 //pos:当前文件指针位置 ALIENTEK MiniSTM32 V3.0 开发板教程 u32 fupd_prog(u16 x,u16 y,u8 size,u32 fsize,u32 pos) { float prog; u8 t=0XFF; prog=(float)pos/fsize; prog*=100; if(t!=prog) { LCD_ShowString(x+3*size/2,y,240,320,size,"%"); t=prog; if(t>100)t=100; LCD_ShowNum(x,y,t,3,size);//显示数值 } return 0; } //更新某一个 //x,y:坐标 //size:字体大小 //fxpath:路径 //fx:更新的内容 0,ungbk;1,gbk12;2,gbk16;3,gbk24; //返回值:0,成功;其他,失败. u8 updata_fontx(u16 x,u16 y,u8 size,u8 *fxpath,u8 fx) { u32 flashaddr=0; u16 bread; u32 offx=0; FIL * fftemp; u8 *tempbuf; u8 res; u8 rval=0; fftemp=(FIL*)mymalloc(sizeof(FIL)); //分配内存 if(fftemp==NULL)rval=1; tempbuf=mymalloc(4096); //分配 4096 个字节空间 if(tempbuf==NULL)rval=1; res=f_open(fftemp,(const TCHAR*)fxpath,FA_READ); if(res)rval=2;//打开文件失败 if(rval==0) { switch(fx) { case 0: //更新 UNIGBK.BIN ftinfo.ugbkaddr=FONTINFOADDR+sizeof(ftinfo);// UNIGBK 转换码表 ftinfo.ugbksize=fftemp->fsize; //UNIGBK 大小 466 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 flashaddr=ftinfo.ugbkaddr; break; case 1: ftinfo.f12addr=ftinfo.ugbkaddr+ftinfo.ugbksize; //GBK12 字库地址 ftinfo.gbk12size=fftemp->fsize; //GBK12 字库大小 flashaddr=ftinfo.f12addr; //GBK12 的起始地址 break; case 2: ftinfo.f16addr=ftinfo.f12addr+ftinfo.gbk12size; ftinfo.gbk16size=fftemp->fsize; flashaddr=ftinfo.f16addr; // GBK16 字库地址 //GBK16 字库大小 //GBK16 的起始地址 break; case 3: ftinfo.f24addr=ftinfo.f16addr+ftinfo.gbk16size; ftinfo.gkb24size=fftemp->fsize; flashaddr=ftinfo.f24addr; // GBK24 字库地址 //GBK24 字库大小 //GBK24 的起始地址 break; } while(res==FR_OK)//死循环执行 { res=f_read(fftemp,tempbuf,4096,(UINT *)&bread); //读取数据 if(res!=FR_OK)break; //执行错误 SPI_Flash_Write(tempbuf,offx+flashaddr,4096); //从 0 开始写入 4096 个数据 offx+=bread; fupd_prog(x,y,size,fftemp->fsize,offx); if(bread!=4096)break; //进度显示 //读完了. } f_close(fftemp); } myfree(fftemp); //释放内存 myfree(tempbuf); //释放内存 return res; } //更新字体文件,UNIGBK,GBK12,GBK16,GBK24 一起更新 //x,y:提示信息的显示地址 //size:字体大小 //提示信息字体大小 //返回值:0,更新成功; // 其他,错误代码. u8 update_font(u16 x,u16 y,u8 size) { u8 *gbk24_path=(u8*)GBK24_PATH; u8 *gbk16_path=(u8*)GBK16_PATH; 467 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 u8 *gbk12_path=(u8*)GBK12_PATH; u8 *unigbk_path=(u8*)UNIGBK_PATH; u8 res; res=0XFF; ftinfo.fontok=0XFF; SPI_Flash_Write((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo)); //清除之前字库成功的标志.防止更新到一半重启,导致的字库部分数据丢失. SPI_Flash_Read((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo)); //重新读出 ftinfo 结构体数据 LCD_ShowString(x,y,240,320,size,"Updating UNIGBK.BIN"); res=updata_fontx(x+20*size/2,y,size,unigbk_path,0); //更新 UNIGBK.BIN if(res)return 1; LCD_ShowString(x,y,240,320,size,"Updating GBK12.BIN "); res=updata_fontx(x+20*size/2,y,size,gbk12_path,1); //更新 GBK12.FON if(res)return 2; LCD_ShowString(x,y,240,320,size,"Updating GBK16.BIN "); res=updata_fontx(x+20*size/2,y,size,gbk16_path,2); //更新 GBK16.FON if(res)return 3; LCD_ShowString(x,y,240,320,size,"Updating GBK24.BIN "); res=updata_fontx(x+20*size/2,y,size,gbk24_path,3); //更新 GBK24.FON if(res)return 4; ftinfo.fontok=0XAA; //全部更新好了 SPI_Flash_Write((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo)); return 0;//无错误. } //初始化字体 //返回值:0,字库完好. // 其他,字库丢失 //保存字库信息 u8 font_init(void) { SPI_Flash_Init(); SPI_Flash_Read((u8*)&ftinfo,FONTINFOADDR,sizeof(ftinfo));//读出 ftinfo 结构体数据 if(ftinfo.fontok!=0XAA)return 1; //字库错误. return 0; } 此部分代码主要用于字库的更新操作(包含 UNIGBK 的转换码表更新),其中 ftinfo 是我 们在 fontupd.h 里面定义的一个结构体,用于记录字库首地址及字库大小等信息。因为我们将 W25Q64 的前 4.8M 字节给 FATFS 管理(用做本地磁盘),然后又预留了 100K 字节给用户自己 使用,最后的 3.1M 字节(W25Q64 总共 8M 字节),才是 UNIGBK 码表和字库的存储空间,所 以,我们的存储地址是从(4916+100)*1024 处开始的。最开始的 33 个字节给 ftinfo 用,用于保 存 ftinfo 结构体数据,之后依次是:UNIGBK.BIN、GBK12.FON、GBK16.FON 和 GBK24.FON。 接下来我们打开 fontupd.h,该文件里面代码如下: #ifndef __FONTUPD_H__ 468 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define __FONTUPD_H__ #include //前面 4.8M 被 fatfs 占用了. //4.8M 以后紧跟的 100K 字节,用户可以随便用. //4.8M+100K 字节以后的字节,被字库占用了,不能动! //字体信息保存地址,占 33 个字节,第 1 个字节用于标记字库是否存在.后续每 8 个字节一组 //分别保存起始地址和文件大小 extern u32 FONTINFOADDR; //字库信息结构体定义 //用来保存字库基本信息,地址,大小等 __packed typedef struct { u8 fontok; u32 ugbkaddr; u32 ugbksize; u32 f12addr; u32 gbk12size; //字库存在标志,0XAA,字库正常;其他,字库不存在 //unigbk 的地址 //unigbk 的大小 //gbk12 地址 //gbk12 的大小 u32 f16addr; u32 gbk16size; //gbk16 地址 //gbk16 的大小 u32 f24addr; u32 gkb24size; //gbk24 地址 //gbk24 的大小 }_font_info; extern _font_info ftinfo; //字库信息结构体 u32 fupd_prog(u16 x,u16 y,u8 size,u32 fsize,u32 pos); u8 updata_fontx(u16 x,u16 y,u8 size,u8 *fxpath,u8 fx); u8 update_font(u16 x,u16 y,u8 size); //显示更新进度 //更新指定字库 //更新全部字库 u8 font_init(void); #endif 这里,我们可以看到 ftinfo 的结构体定义,总共占用 25 个字节,第一个字节用来标识字库 是否 OK,其他的用来记录地址和文件大小。接下来打开 text.c 文件,该文件代码如下: #include "sys.h" #include "fontupd.h" #include "flash.h" #include "lcd.h" #include "text.h" #include "string.h" //code 字符指针开始 //从字库中查找出字模 //code 字符串的开始地址,GBK 码 //mat 数据存放地址 (size/8+((size%8)?1:0))*(size) bytes 大小 //size:字体大小 void Get_HzMat(unsigned char *code,unsigned char *mat,u8 size) { 469 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 unsigned char qh,ql; unsigned char i; unsigned long foffset; u8 csize=(size/8+((size%8)?1:0))*(size);//得到该字体一个汉字对应点阵集所占字节数 qh=*code; ql=*(++code); if(qh<0x81||ql<0x40||ql==0xff||qh==0xff)//非 常用汉字 { for(i=0;ilcddev.width)||(y0>lcddev.height))return; LCD_Fill(x0,y0,x0+len-1,y0,color); } //填充颜色 //x,y:起始坐标 //width,height:宽度和高度。 //*color:颜色数组 void piclib_fill_color(u16 x,u16 y,u16 width,u16 height,u16 *color) { LCD_Color_Fill(x,y,x+width-1,y+height-1,color); } //画图初始化,在画图之前,必须先调用此函数 //指定画点/读点 void piclib_init(void) { pic_phy.read_point=LCD_ReadPoint; //读点函数实现 pic_phy.draw_point=LCD_Fast_DrawPoint; //画点函数实现 pic_phy.fill=LCD_Fill; //填充函数实现 pic_phy.draw_hline=piclib_draw_hline; //画线函数实现 pic_phy.fillcolor=piclib_fill_color; //颜色填充函数实现 picinfo.lcdwidth=lcddev.width; //得到 LCD 的宽度像素 picinfo.lcdheight=lcddev.height; //得到 LCD 的高度像素 picinfo.ImgWidth=0; //初始化宽度为 0 picinfo.ImgHeight=0; picinfo.Div_Fac=0; picinfo.S_Height=0; picinfo.S_Width=0; picinfo.S_XOFF=0; //初始化高度为 0 //初始化缩放系数为 0 //初始化设定的高度为 0 //初始化设定的宽度为 0 //初始化 x 轴的偏移量为 0 picinfo.S_YOFF=0; //初始化 y 轴的偏移量为 0 480 STM32 不完全手册(库函数版) picinfo.staticx=0; picinfo.staticy=0; ALIENTEK MiniSTM32 V3.0 开发板教程 //初始化当前显示到的 x 坐标为 0 //初始化当前显示到的 y 坐标为 0 } //快速 ALPHA BLENDING 算法. //src:源颜色 //dst:目标颜色 //alpha:透明程度(0~32) //返回值:混合后的颜色. u16 piclib_alpha_blend(u16 src,u16 dst,u8 alpha) { u32 src2; u32 dst2; //Convert to 32bit |-----GGGGGG-----RRRRR------BBBBB| src2=((src<<16)|src)&0x07E0F81F; dst2=((dst<<16)|dst)&0x07E0F81F; //Perform blending R:G:B with alpha in range 0..32 //Note that the reason that alpha may not exceed 32 is that there are only //5bits of space between each R:G:B value, any higher value will overflow //into the next component and deliver ugly result. dst2=((((dst2-src2)*alpha)>>5)+src2)&0x07E0F81F; return (dst2>>16)|dst2; } //初始化智能画点 //内部调用 void ai_draw_init(void) { float temp,temp1; temp=(float)picinfo.S_Width/picinfo.ImgWidth; temp1=(float)picinfo.S_Height/picinfo.ImgHeight; if(temp1)temp1=1; //使图片处于所给区域的中间 picinfo.S_XOFF+=(picinfo.S_Width-temp1*picinfo.ImgWidth)/2; picinfo.S_YOFF+=(picinfo.S_Height-temp1*picinfo.ImgHeight)/2; temp1*=8192;//扩大 8192 倍 picinfo.Div_Fac=temp1; picinfo.staticx=0xffff; picinfo.staticy=0xffff;//放到一个不可能的值上面 } //判断这个像素是否可以显示 //(x,y) :像素原始坐标 //chg :功能变量. //返回值:0,不需要显示.1,需要显示 u8 is_element_ok(u16 x,u16 y,u8 chg) 481 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { if(x!=picinfo.staticx||y!=picinfo.staticy) { if(chg==1) { picinfo.staticx=x; picinfo.staticy=y; } return 1; }else return 0; } //智能画图 //FileName:要显示的图片文件 BMP/JPG/JPEG/GIF //x,y,width,height:坐标及显示区域尺寸 //fast:使能 jpeg/jpg 小图片(图片尺寸小于等于液晶分辨率)快速解码,0,不使能;1,使能. //图片在开始和结束的坐标点范围内显示 u8 ai_load_picfile(const u8 *filename,u16 x,u16 y,u16 width,u16 height,u8 fast) { u8 res;//返回值 u8 temp; if((x+width)>picinfo.lcdwidth)return PIC_WINDOW_ERR; //x 坐标超范围了. if((y+height)>picinfo.lcdheight)return PIC_WINDOW_ERR; //y 坐标超范围了. //得到显示方框大小 if(width==0||height==0)return PIC_WINDOW_ERR; //窗口设定错误 picinfo.S_Height=height; picinfo.S_Width=width; //显示区域无效 if(picinfo.S_Height==0||picinfo.S_Width==0) { picinfo.S_Height=lcddev.height; picinfo.S_Width=lcddev.width; return FALSE; } if(pic_phy.fillcolor==NULL)fast=0;//颜色填充函数未实现,不能快速显示 //显示的开始坐标点 picinfo.S_YOFF=y; picinfo.S_XOFF=x; //文件名传递 temp=f_typetell((u8*)filename); switch(temp) //得到文件的类型 { case T_BMP: res=stdbmp_decode(filename); //解码 bmp 482 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 break; case T_JPG: case T_JPEG: res=jpg_decode(filename,fast); //解码 JPG/JPEG break; case T_GIF: res=gif_decode(filename,x,y,width,height); //解码 gif break; default: res=PIC_FORMAT_ERR; //非图片格式!!! break; } return res; } 此段代码总共 7 个函数,其中,piclib_draw_hline 和 piclib_fill_color 函数因为 LCD 驱动代 码没有提供,所以在这里单独实现,如果 LCD 驱动代码有提供,则直接用 LCD 提供的即可。 piclib_init 函数,该函数用于初始化图片解码的相关信息,其中_pic_phy 是我们在 piclib.h 里面定义的一个结构体,用于管理底层 LCD 接口函数,这些函数必须由用户在外部实现。 _pic_info 则是另外一个结构体,用于图片缩放处理。 piclib_alpha_blend 函数,该函数用于实现半透明效果,在小格式(分辨率小于 240*320) bmp 解码的时候,可能被用到。 ai_draw_init 函数,该函数用于实现图片在显示区域的居中显示初始化,其实就是根据图片 大小选择缩放比例和坐标偏移值。 is_element_ok 函数,该函数用于判断一个点是不是应该显示出来,在图片缩放的时候该函 数是必须用到的。 ai_load_picfile 函数,该函数是整个图片显示的对外接口,外部程序,通过调用该函数,可 以实现 bmp、jpg/jpeg 和 gif 的显示,该函数根据输入文件的后缀名,判断文件格式,然后交给 相应的解码程序(bmp 解码/jpeg 解码/gif 解码),执行解码,完成图片显示。注意,这里我们用 到一个 f_typetell 的函数,来判断文件的后缀名,f_typetell 函数在 exfuns.c 里面实现,具体请参 考光盘源码。 接下来打开 piclib.h, 该文件代码如下: #ifndef __PICLIB_H #define __PICLIB_H #include "sys.h" #include "lcd.h" #include "malloc.h" #include "ff.h" #include "exfuns.h" #include "bmp.h" #include "tjpgd.h" #include "gif.h" #define PIC_FORMAT_ERR #define PIC_SIZE_ERR 0x27 0x28 //格式错误 //图片尺寸错误 483 STM32 不完全手册(库函数版) #define PIC_WINDOW_ERR #define PIC_MEM_ERR 0x29 0x11 ALIENTEK MiniSTM32 V3.0 开发板教程 //窗口设定错误 //内存错误 #ifndef TRUE #define TRUE 1 #endif #ifndef FALSE #define FALSE 0 #endif //图片显示物理层接口 //在移植的时候,必须由用户自己实现这几个函数 typedef struct { u16(*read_point)(u16,u16); //u16 read_point(u16 x,u16 y) 读点函数 void(*draw_point)(u16,u16,u16); //void draw_point(u16 x,u16 y,u16 color) 画点函数 void(*fill)(u16,u16,u16,u16,u16); //void fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 color) 单色填充函数 void(*draw_hline)(u16,u16,u16,u16); //void draw_hline(u16 x0,u16 y0,u16 len,u16 color) 画水平线函数 void(*fillcolor)(u16,u16,u16,u16,u16*); //void piclib_fill_color(u16 x,u16 y,u16 width,u16 height,u16 *color) 颜色填充 }_pic_phy; extern _pic_phy pic_phy; //图像信息 typedef struct { u16 lcdwidth; u16 lcdheight; u32 ImgWidth; //LCD 的宽度 //LCD 的高度 //图像的实际宽度和高度 u32 ImgHeight; u32 Div_Fac; u32 S_Height; //缩放系数 (扩大了 8192 倍的) //设定的高度和宽度 u32 S_Width; u32 S_XOFF; //x 轴和 y 轴的偏移量 u32 S_YOFF; u32 staticx; //当前显示到的xy坐标 u32 staticy; }_pic_info; extern _pic_info picinfo;//图像信息 void piclib_init(void); //初始化画图 u16 piclib_alpha_blend(u16 src,u16 dst,u8 alpha); //alphablend 处理 void ai_draw_init(void); //初始化智能画图 u8 is_element_ok(u16 x,u16 y,u8 chg); //判断像素是否有效 u8 ai_load_picfile(const u8 *filename,u16 x,u16 y,u16 width,u16 height,u8 fast);//智能画图 484 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #endif 这里基本就是我们前面提到的两个结构体的定义以及一些函数的申明。最后我们看看 main.c 文件内容 //得到 path 路径下,目标文件的总个数 //path:路径 //返回值:总有效文件数 u16 pic_get_tnum(u8 *path) { u8 res; u16 rval=0; DIR tdir; //临时目录 FILINFO tfileinfo; //临时文件信息 u8 *fn; res=f_opendir(&tdir,(const TCHAR*)path); //打开目录 tfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度 tfileinfo.lfname=mymalloc(tfileinfo.lfsize);//为长文件缓存区分配内存 if(res==FR_OK&&tfileinfo.lfname!=NULL) { while(1)//查询总的有效文件数 { res=f_readdir(&tdir,&tfileinfo); if(res!=FR_OK||tfileinfo.fname[0]==0)break; //读取目录下的一个文件 //错误了/到末尾了,退出 fn=(u8*)(*tfileinfo.lfname?tfileinfo.lfname:tfileinfo.fname); res=f_typetell(fn); if((res&0XF0)==0X50)//取高四位,看看是不是图片文件 { rval++;//有效文件数增加 1 } } } return rval; } int main(void) { u8 res; DIR picdir; //图片目录 FILINFO picfileinfo;//文件信息 u8 *fn; //长文件名 u8 *pname; u16 totpicnum; //带路径的文件名 //图片文件总数 u16 curindex; u8 key; //图片当前索引 //键值 u8 pause=0; //暂停标记 u8 t; 485 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 u16 temp; u16 *picindextbl; delay_init(); uart_init(9600); LCD_Init(); LED_Init(); KEY_Init(); //图片索引表 //延时函数初始化 //串口初始化为 9600 //初始化液晶 //LED 初始化 //按键初始化 usmart_dev.init(72);//usmart 初始化 mem_init(); //初始化内部内存池 exfuns_init(); //为 fatfs 相关变量申请内存 f_mount(fs[0],"0:",1); //挂载 SD 卡 f_mount(fs[1],"1:",1); //挂载 FLASH. POINT_COLOR=RED; while(font_init()) //检查字库 { LCD_ShowString(60,50,200,16,16,"Font Error!"); delay_ms(200); LCD_Fill(60,50,240,66,WHITE); delay_ms(200); } Show_Str(60,50,200,16,"Mini STM32 开发板",16,0); Show_Str(60,70,200,16,"图片显示程序",16,0); Show_Str(60,90,200,16,"KEY0:NEXT KEY1:PREV",16,0); Show_Str(60,110,200,16,"WK_UP:PAUSE",16,0); Show_Str(60,130,200,16,"正点原子@ALIENTEK",16,0); Show_Str(60,150,200,16,"2014 年 3 月 14 日",16,0); while(f_opendir(&picdir,"0:/PICTURE"))//打开图片文件夹 { Show_Str(60,170,240,16,"PICTURE 文件夹错误!",16,0);delay_ms(200); LCD_Fill(60,170,240,186,WHITE); delay_ms(200); } totpicnum=pic_get_tnum("0:/PICTURE"); //得到总有效文件数 while(totpicnum==NULL)//图片文件为 0 { Show_Str(60,170,240,16,"没有图片文件!",16,0); delay_ms(200); LCD_Fill(60,170,240,186,WHITE); delay_ms(200);//清除显示 } picfileinfo.lfsize=_MAX_LFN*2+1; //长文件名最大长度 picfileinfo.lfname=mymalloc(picfileinfo.lfsize); //为长文件缓存区分配内存 pname=mymalloc(picfileinfo.lfsize); //为带路径的文件名分配内存 picindextbl=mymalloc(2*totpicnum);//申请 2*totpicnum 个字节内存,用于存放图片索引 while(picfileinfo.lfname==NULL||pname==NULL||picindextbl==NULL)//内存分配出错 { Show_Str(60,170,240,16,"内存分配失败!",16,0); delay_ms(200); 486 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_Fill(60,170,240,186,WHITE); delay_ms(200); } //记录索引 res=f_opendir(&picdir,"0:/PICTURE"); //打开目录 if(res==FR_OK) { curindex=0;//当前索引为 0 while(1)//全部查询一遍 { temp=picdir.index; //记录当前 index res=f_readdir(&picdir,&picfileinfo); //读取目录下的一个文件 if(res!=FR_OK||picfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*picfileinfo.lfname?picfileinfo.lfname:picfileinfo.fname); res=f_typetell(fn); if((res&0XF0)==0X50)//取高四位,看看是不是图片文件 { picindextbl[curindex]=temp;//记录索引 curindex++; } } } Show_Str(60,170,240,16,"开始显示...",16,0); delay_ms(1500); piclib_init(); //初始化画图 curindex=0; //从 0 开始显示 res=f_opendir(&picdir,(const TCHAR*)"0:/PICTURE"); //打开目录 while(res==FR_OK)//打开成功 { dir_sdi(&picdir,picindextbl[curindex]); //改变当前目录索引 res=f_readdir(&picdir,&picfileinfo); //读取目录下的一个文件 if(res!=FR_OK||picfileinfo.fname[0]==0)break; //错误了/到末尾了,退出 fn=(u8*)(*picfileinfo.lfname?picfileinfo.lfname:picfileinfo.fname); strcpy((char*)pname,"0:/PICTURE/"); //复制路径(目录) strcat((char*)pname,(const char*)fn); //将文件名接在后面 LCD_Clear(BLACK); ai_load_picfile(pname,0,0,lcddev.width,lcddev.height,1);//显示图片 Show_Str(2,2,240,16,pname,16,1); //显示图片名字 t=0; while(1) { key=KEY_Scan(0); //扫描按键 if(t>250)key=1; //模拟一次按下 KEY0 if((t%20)==0)LED0=!LED0;//LED0 闪烁,提示程序正在运行. 487 STM32 不完全手册(库函数版) if(key==KEY1_PRES) ALIENTEK MiniSTM32 V3.0 开发板教程 //上一张 { if(curindex)curindex--; else curindex=totpicnum-1; break; }else if(key==KEY0_PRES)//下一张 { curindex++; if(curindex>=totpicnum)curindex=0;//到末尾的时候,自动从头开始 break; }else if(key==WKUP_PRES) { pause=!pause; LED1=!pause; //暂停的时候 LED1 亮. } if(pause==0)t++; delay_ms(10); } res=0; } myfree(picfileinfo.lfname); myfree(pname); myfree(picindextbl); //释放内存 //释放内存 //释放内存 } 此部分除了 mian 函数,还有一个 pic_get_tnum 的函数,用来得到 path 路径下,所有有效 文件(图片文件)的个数。在 mian 函数里面我们通过索引(图片文件在 PICTURE 文件夹下的 编号),来查找上一个/下一个图片文件,这里我们需要用到 fatfs 自带的一个函数:dir_sdi,来 设置当前目录的索引(因为 f_readdir 只能沿着索引一直往下找,不能往上找),方便定位到任 何一个文件。dir_sdi 在 FATFS 下面被定义为 static 函数,所以我们必须在 ff.c 里面将该函数的 static 修饰词去掉,然后在 ff.h 里面添加该函数的申明,以便 main 函数使用。 其他部分就比较简单了,至此,整个图片显示实验的软件设计部分就结束了。该程序将实 现浏览 PICTURE 文件夹下的所有图片,并显示其名字,每隔 3s 左右切换一幅图片。 36.4 下载验证 在代码编译成功之后,我们下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到 LCD 开始显示图片(假设 SD 卡及图片文件都已经准备好了),如图 36.4.1 所示: 488 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 36.4.1 图片显示实验显示效果 按 KEY0 和 KEY1 可以快速切换到下一张或上一张,WK_UP 按键可以暂停自动播放,同 时 DS1 亮,指示处于暂停状态,再按一次 WK_UP 则继续播放(DS1 灭)。同时,由于我们的 代码支持 gif 格式的图片显示(注意尺寸不能超过 LCD 屏幕尺寸),所以可以放一些 gif 图片到 PICTURE 文件夹,来看动画了。 489 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三十七章 串口 IAP 实验 IAP,即在应用编程。很多单片机都支持这个功能,STM32 也不例外。在之前的 FLASH 模拟 EEPROM 实验里面,我们学习了 STM32 的 FLASH 自编程,本章我们将结合 FLASH 自编 程的知识,通过 STM32 的串口实现一个简单的 IAP 功能本章分为如下几个部分: 37.1 IAP 简介 37.2 硬件设计 37.3 软件设计 37.4 下载验证 490 37.1 IAP 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 IAP(In Application Programming)即在应用编程,IAP 是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产 品中的固件程序进行更新升级。 通常实现 IAP 功能时,即用户程序运行中作自身的更新操作, 需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通 过某种通信方式(如 USB、USART)接收程序或数据,执行对第二部分代码的更新;第二个项目 代码才是真正的功能代码。这两部分项目代码都同时烧录在 User Flash 中,当芯片上电后,首 先是第一个项目代码开始运行,它作如下操作: 1)检查是否需要对第二部分代码进行更新 2)如果不需要更新则转到 4) 3)执行更新操作 4)跳转到第二部分代码执行 第一部分代码必须通过其它手段,如 JTAG 或 ISP 烧入;第二部分代码可以使用第一部分 代码 IAP 功能烧入,也可以和第一部分代码一起烧入,以后需要程序更新时再通过第一部分 IAP 代码更新。 我们将第一个项目代码称之为 Bootloader 程序,第二个项目代码称之为 APP 程序,他们存 放在 STM32 FLASH 的不同地址范围,一般从最低地址区开始存放 Bootloader,紧跟其后的就 是 APP 程序(注意,如果 FLASH 容量足够,是可以设计很多 APP 程序的,本章我们只讨论一 个 APP 程序的情况)。这样我们就是要实现 2 个程序:Bootloader 和 APP。 STM32 的 APP 程序不仅可以放到 FLASH 里面运行,也可以放到 SRAM 里面运行,本章, 我们将制作两个 APP,一个用于 FLASH 运行,一个用于 SRAM 运行。 我们先来看看 STM32 正常的程序运行流程,如图 37.1.1 所示: 0X08000000 闪存物理地址 栈顶地址 复位中断向量 0X08000004 (中断向量表起始地址) Reset_Handler 非可屏蔽中断向量 NMIEeception ① 硬件错误中断向量 HardFaultException 递增 … … 0X08000004+n 复位中断程序入口 Reset_Handler(void) 硬件错误中断程序入口 HardFaultException(void) … … ② xxx中断程序入口 xxx_Handler(void) … … 0X08000004+N main函数入口 int main(void) ④ ③ main函数 死循环 ⑤ 中断请求 图 37.1.1 STM32 正常运行流程图 STM32 的内部闪存(FLASH)地址起始于 0x08000000,一般情况下,程序文件就从此地 491 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 址开始写入。此外 STM32 是基于 Cortex-M3 内核的微控制器,其内部通过一张“中断向量表” 来响应中断,程序启动后,将首先从“中断向量表”取出复位中断向量执行复位中断程序完成 启动,而这张“中断向量表”的起始地址是 0x08000004,当中断来临,STM32 的内部硬件机 制亦会自动将 PC 指针定位到“中断向量表”处,并根据中断源取出对应的中断向量执行中断 服务程序。 在图 37.1.1 中,STM32 在复位后,先从 0X08000004 地址取出复位中断向量的地址,并跳 转到复位中断服务程序,如图标号①所示;在复位中断服务程序执行完之后,会跳转到我们的 main 函数,如图标号②所示;而我们的 main 函数一般都是一个死循环,在 main 函数执行过程 中,如果收到中断请求(发生重中断),此时 STM32 强制将 PC 指针指回中断向量表处,如图 标号③所示;然后,根据中断源进入相应的中断服务程序,如图标号④所示;在执行完中断服 务程序以后,程序再次返回 main 函数执行,如图标号⑤所示。 当加入 IAP 程序之后,程序运行流程如图 37.1.2 所示: 0X08000000 闪存物理地址 栈顶地址 0X08000004 复位中断向量 (中断向量表起始地址) Reset_Handler 非可屏蔽中断向量 NMIEeception ① 硬件错误中断向量 HardFaultException 递增 … … 0X08000004+N IAP程序main函数入口 int main(void) 递增 IAP过程 0X08000004+N+M 跳转② 复位中断向量 (新中断向量表起始地址) Reset_Handler 非可屏蔽中断向量 NMIEeception 硬件错误中断向量 HardFaultException 递增 … … ③ 复位中断程序入口 Reset_Handler(void) 硬件错误中断程序入口 HardFaultException(void) … … xxx中断程序入口 xxx_Handler(void) … … 0X08000004+N+M+n 新程序main函数入口 int main(void) ⑤ ④ main函数 死循环 ⑥ 中断请求 图 37.1.2 加入 IAP 之后程序运行流程图 在图 37.1.2 所示流程中,STM32 复位后,还是从 0X08000004 地址取出复位中断向量的地 492 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数, 如图标号①所示,此部分同图 37.1.1 一样;在执行完 IAP 以后(即将新的 APP 代码写入 STM32 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0X08000004+N+M),跳转至新写入 程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程 序,随后跳转至新程序的 main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并 且注意到此时 STM32 的 FLASH,在不同位置上,共有两个中断向量表。 在 main 函数执行过程中,如果 CPU 得到一个中断请求,PC 指针仍强制跳转到地址 0X08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;程序再根据我们设 置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;在执行完 中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。 通过以上两个过程的分析,我们知道 IAP 程序必须满足两个要求: 1) 新程序必须在 IAP 程序之后的某个偏移量为 x 的地址开始; 2) 必须将新程序的中断向量表相应的移动,移动的偏移量为 x; 本章,我们有 2 个 APP 程序,一个是 FLASH 的 APP,另外一个是 SRAM 的 APP,图 37.1.2 虽然是针对 FLASH APP 来说的,但是在 SRAM 里面运行的过程和 FLASH 基本一致,只是需 要设置向量表的地址为 SRAM 的地址。 1.APP 程序起始地址设置方法 随便打开一个之前的实例工程,点击 Options for TargetTarget 选项卡,如图 37.1.3 所示: 493 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 37.1.3 FLASH APP Target 选项卡设置 默认的条件下,图中 IROM1 的起始地址(Start)一般为 0X08000000,大小(Size)为 0X40000, 即从 0X08000000 开始的 256K 空间为我们的程序存储区。而图中,我们设置起始地址(Start) 为 0X08010000,即偏移量为 0X10000(64K 字节),因而,留给 APP 用的 FLASH 空间(Size) 只有 0X80000-0X10000=0X30000(192K 字节)大小了。设置好 Start 和 Szie,就完成 APP 程序 的起始地址设置。 这里的 64K 字节不是固定的,大家可以根据 Bootloader 程序大小进行不同设置,理论上我 们只需要确保 APP 起始地址在 Bootloader 之后,并且偏移量为 0X200 的倍数即可(相关知识, 请参考:http://www.openedv.com/posts/list/392.htm)。比如我们本章的 Bootloader 程序为 30K 左 右,设置为 64K,还留有 20K 左右的余量供后续在 IAP 里面新增其他功能之用。 以上针对 FLASH APP 的起始地址设置,如果是 SRAM APP,那么起始地址设置如图 37.1.4 所示: 494 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 37.1.4 SRAM APP Target 选项卡设置 这里我们将 IROM1 的起始地址(Start)定义为:0X20001000,大小为 0XA000(40K 字节), 即从地址 0X20000000 偏移 0X1000 开始,之后的 40K 字节,用于存放 APP 代码。因为整个 STM32F103RCT6 的 SRAM 大小为 48K 字节,且偏移了 4K(0X1000),所以 IRAM1(SRAM) 的起始地址变为 0X2000B000(0X1000+0XA000),大小只有 0X1000(4K 字节)。 这样,整个 STM32F103RCT6 的 SRAM 分配情况为:最开始的 4K 给 Bootloader 程序使用, 随后的 40K 存放 APP 程序,最后 4K,用作 APP 程序的内存。 这个分配关系大家可以根据自己的实际情况修改,不一定和我们这里的设置一模一样,不 过需要满足以下 4 个条件: 1, 保证偏移量为 0X200 的倍数(我们这里为 0X1000)。 2, IROM1 的容量最大为 41KB(因为 IAP 代码里面接收数组最大是 41K 字节)。 3, IROM1 的地址区域和 IRAM1 的地址区域不能重叠。 4, IROM1 大小+IRAM1 大小,不要超过 44KB(48K-4K)。 2.中断向量表的偏移量设置方法 之前我们讲解过,在系统启动的时候,会首先调用 systemInit 函数初始化时钟系统,同时 systemInit 还完成了中断向量表的设置,我们可以打开 systemInit 函数,看看函数体的结尾处有 这样几行代码: #ifdef VECT_TAB_SRAM SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */ #else SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; 495 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 /* Vector Table Relocation in Internal FLASH. */ #endif 从 代 码 可 以 理 解 , VTOR 寄 存 器 存 放 的 是 中 断 向 量 表 的 起 始 地 址 。 默 认 的 情 况 VECT_TAB_SRAM 是没有定义,所以执行 SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; 对于 FLASH APP,我们设置为 FLASH_BASE+偏移量 0x10000,所以我们可以在 FLASH APP 的 main 函数最开头处添加如下代码实现中断向量表的起始地址的重设: SCB->VTOR = FLASH_BASE | 0x10000; 以上是 FLASH APP 的情况,当使用 SRAM APP 的时候,我们设置起始地址为: SRAM_bASE+0x1000,同样的方法,我们在 SRAM APP 的 main 函数最开始处,添加下面代码: SCB->VTOR = SRAM_BASE | 0x1000; 这样,我们就完成了中断向量表偏移量的设置。 通过以上两个步骤的设置,我们就可以生成 APP 程序了,只要 APP 程序的 FLASH 和 SRAM 大小不超过我们的设置即可。不过 MDK 默认生成的文件是.hex 文件,并不方便我们用作 IAP 更新,我们希望生成的文件是.bin 文件,这样可以方便进行 IAP 升级(至于为什么,请大家自 行百度 HEX 和 BIN 文件的区别!)。这里我们通过 MDK 自带的格式转换工具 fromelf.exe,来实 现.axf 文件到.bin 文件的转换。该工具在 MDK 的安装目录\ARM\BIN40 文件夹里面。 fromelf.exe 转换工具的语法格式为:fromelf [options] input_file。其中 options 有很多选项可 以设置,详细使用请参考光盘《mdk 如何生成 bin 文件.doc》. 本章,我们通过在 MDK 点击 Options for TargetUser 选项卡,在 Run User Programs After Build/Rebuild 栏,勾选 Run#1,并写入:D:\MDK5.10\ARM\BIN40\fromelf.exe --bin -o ..\OBJ \TEST.bin ..\OBJ\TEST.axf ,如图 37.1.6 所示: 通过这一步设置,我们就可以在 MDK 编译成功之后,调用 fromelf.exe(注意,我的 MDK 496 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 是安装在 D:\MDK5.10 文件夹下,如果你是安装在其他目录,请根据你自己的目录修改 fromelf.exe 的路径),根据当前工程的 TEST.axf,生成一个 TEST.bin 的文件。并存放在 axf 文 件相同的目录下,即工程的 OBJ 文件夹里面。在得到.bin 文件之后,我们只需要将这个 bin 文 件传送给单片机,即可执行 IAP 升级。 最后再来看看 APP 程序的生成步骤: 1) 设置 APP 程序的起始地址和存储空间大小 对于在 FLASH 里面运行的 APP 程序,我们只需要设置 APP 程序的起始地址,和存储 空间大小即可。而对于在 SRAM 里面运行的 APP 程序,我们还需要设置 SRAM 的起始地 址和大小。无论哪种 APP 程序,都需要确保 APP 程序的大小和所占 SRAM 大小不超过我 们的设置范围。 2) 设置中断向量表偏移量 这一步按照上面讲解,重新设置 SCB->VTOR 的值即可。 3) 设置编译后运行 fromelf.exe,生成.bin 文件. 通过在 User 选项卡,设置编译后调用 fromelf.exe,根据.axf 文件生成.bin 文件,用于 IAP 更新。 通过以上 3 个步骤,我们就可以得到一个.bin 的 APP 程序,通过 Bootlader 程序即可实现 更新。 37.2 硬件设计 本章实验(Bootloader 部分)功能简介:开机的时候先显示提示信息,然后等待串口输入 接收 APP 程序(无校验,一次性接收),在串口接收到 APP 程序之后,即可执行 IAP。如果 是 SRAM APP,通过按下 KEY0 即可执行这个收到的 SRAM APP 程序。如果是 FLASH APP, 则需要先按下 WK_UP 按键,将串口接收到的 APP 程序存放到 STM32 的 FLASH,之后再按 KEY1 既可以执行这个 FLASH APP 程序。DS0 用于指示程序运行状态。 本实验用到的资源如下: 1) 指示灯 DS0 2) 三个按键(KEY0/KEY1/WK_UP) 3) 串口 4) TFTLCD 模块 这些用到的硬件,我们在之前都已经介绍过,这里就不再介绍了。 37.3 软件设计 本章,我们总共需要 3 个程序:1,Bootloader;2,FLASH APP;3)SRAM APP;其中, 我们选择之前做过的 RTC 实验(在第十三章介绍)来做为 FLASH APP 程序(起始地址为 0X08010000),选择跑马灯实验(在第六章介绍)来做 SRAM APP 程序(起始地址为 0X20001000)。 Bootloader 则是通过 TFTLCD 显示实验(在第十六章介绍)修改得来。本章,关于 SRAM APP 和 FLASH APP 的生成比较简单,我们就不细说,请大家结合光盘源码,以及 37.1 节的介绍, 自行理解。本章软件设计仅针对 Bootloader 程序。 打开本实验工程,可以看到我们增加了 IAP 组,在组下面添加了 iap.c 文件以及其头文件 isp.h。 打开 iap.c,代码如下: #include "sys.h" #include "delay.h" 497 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #include "usart.h" #include "stmflash.h" #include "iap.h" iapfun jump2app; u16 iapbuf[1024]; //appxaddr:应用程序的起始地址 //appbuf:应用程序 CODE. //appsize:应用程序大小(字节). void iap_write_appbin(u32 appxaddr,u8 *appbuf,u32 appsize) { u16 t; u16 i=0; u16 temp; u32 fwaddr=appxaddr;//当前写入的地址 u8 *dfu=appbuf; for(t=0;tx2)return x1-x2; else return x2-x1; } //设置 USB 连接/断线 //enable:0,断开 // 1,允许连接 void usb_port_set(u8 enable) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); if(enable)_SetCNTR(_GetCNTR()&(~(1<<1)));//退出断电模式 else { _SetCNTR(_GetCNTR()|(1<<1)); // 断电模式 GPIOA->CRH&=0XFFF00FFF; GPIOA->CRH|=0X00033000; PAout(12)=0; } } int main(void) { 509 u8 key; u8 i=0; s8 x0; s8 y0; u8 keysta; u8 tpsta=0; short xlast; short ylast; STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 //发送到电脑端的坐标值 //[0]:0,左键松开;1,左键按下; //[1]:0,右键松开;1,右键按下 //[2]:0,中键松开;1,中键按下 //0,触摸屏第一次按下;1,触摸屏滑动 //最后一次按下的坐标值 delay_init(); //延时函数初始化 NVIC_Configuration();//中断分组设置 uart_init(9600); //串口初始化为 9600 LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 TP_Init(); //初始化触摸屏 POINT_COLOR=RED; LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"USB Mouse TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/15"); LCD_ShowString(60,130,200,16,16,"KEY_UP:SCROLL +"); LCD_ShowString(60,150,200,16,16,"KEY1:RIGHT BTN"); LCD_ShowString(60,170,200,16,16,"KEY0:LEFT BTN"); delay_ms(1800); usb_port_set(0); delay_ms(300); //USB 先断开 usb_port_set(1); //USB 再次连接 USB_Interrupts_Config(); //USB 中断配置 Set_USBClock(); //USB 时钟设置 USB_Init(); //USB 初始化 Load_Draw_Dialog(); while(1) { key=KEY_Scan(1);//支持连按 if(key) { if(key==3)Joystick_Send(0,0,0,1); //发送滚轮数据到电脑 else { if(key==1)keysta|=0X01; //发送鼠标左键 if(key==2)keysta|=0X02; //发送鼠标右键 510 STM32 不完全手册(库函数版) Joystick_Send(keysta,0,0,0); ALIENTEK MiniSTM32 V3.0 开发板教程 //发送给电脑 } }else if(keysta)//之前有按下 { keysta=0; Joystick_Send(0,0,0,0); //发送松开命令给电脑 } tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) //触摸屏被按下 { //最少移动 5 个单位,才算滑动 if(((usb_abs(tp_dev.x[0],xlast)>4)||(usb_abs(tp_dev.y[0],ylast)>4))&&tpsta==0) { xlast=tp_dev.x[0]; //记录刚按下的坐标 ylast=tp_dev.y[0]; tpsta=1; } if(tp_dev.x[0](lcddev.width-24)&&tp_dev.y[0]<16)Load_Draw_Dialog(); else TP_Draw_Big_Point(tp_dev.x[0],tp_dev.y[0],RED); //画图 if(bDeviceState==CONFIGURED) { if(tpsta)//滑动 { x0=(xlast-tp_dev.x[0])*3;//上次坐标与得到的坐标之差,扩大 2 倍 y0=(ylast-tp_dev.y[0])*3; xlast=tp_dev.x[0]; //记录刚按下的坐标 ylast=tp_dev.y[0]; Joystick_Send(keysta,-x0,-y0,0); //发送数据到电脑 delay_ms(5); } } } }else { tpsta=0;delay_ms(1); }//清除 if(bDeviceState==CONFIGURED)LED1=0;//当 USB 配置成功,LED1 亮,否则灭 else LED1=1; i++; if(i==200) { i=0; LED0=!LED0; } } } 在此部分代码用于实现我们在硬件设计部分提到的功能,USB 的配置通过三个函数完成: USB_Interrupts_Config()、Set_USBClock()和 USB_Init(),第一个函数用于设置 USB 唤醒中断和 511 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 USB 低优先级数据处理中断,Set_USBClock 函数用于 配置 USB 时钟,也就是从 72M 的主频 得到 48M 的 USB 时钟(1.5 分频)。最后 USB_Init()函数用于初始化 USB,最主要的就是调用 了 Joystick_init 函数,开启了 USB 部分的电源等。这里需要特别说明的是,USB 配置并没有对 PA11 和 PA12 这两个 IO 口进行设置,是因为,一旦开启了 USB 电源(USB_CNTR 的 PDWN 位清零)PA11 和 PA12 将不再作为其他功能使用,仅供 USB 使用,所以在开启了 USB 电源之 后不论你怎么配置这两个 IO 口,都是无效的。要在此获取这两个 IO 口的配置权,则需要关闭 USB 电源,也就是置位 USB_CNTR 的 PDWN 位,我们通过 usb_port_set 函数来禁止/允许 USB 连接,在复位的时候,先禁止,再允许,这样每次我们按复位电脑都可以识别到 USB 鼠标,而 不需要我们每次都拔 USB 线。 USB 数据发送,我们采用 Joystick_Send 来实现,我们将得到的鼠标数据,在 Joystick_Send 函数里面打包,并通过 USB 端点 1 发送到电脑。 软件设计部分,就给大家介绍到这里。 38.4 下载验证 在代码编译成功之后,我们下载代码到 ALIENTEK MiniSTM32 开发板上,在 USB 没有配 置成功的时候,其界面同第二十六章的实验是一模一样的,如图 38.4.1 所示: 图 38.4.1 USB 无连接时的界面 此时 DS1 不亮,DS0 闪烁,其实就是一个触摸屏画图的功能,而一旦我们将 USB 连接上 (将 USB 线接到侧面的 USB 接口上,而不是 USB_232 接口),则可以看到 DS1 亮了,而且在 电脑上会提示发现新硬件如图 38.4.2 所示: 512 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 38.4.2 电脑提示找到新硬件 在硬件安装完成之后,我们在设备管理器里面可以发现多出了一个人体学输入设备,如图 38.4.3 所示: 图 38.4.3 USB 人体学输入设备 此时我们按动触摸屏,就可以发现电脑屏幕上的光标随着你在触摸屏上的移动而移动了, 同时可以通过按键 KEY0 和 KEY1 模拟鼠标左键和右键,通过按键 WK_UP 模拟鼠标滚轮(只 支持向上滚动)。 513 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第三十九章 USB 读卡器实验 上一章我们向大家介绍了如何利用 STM32 的 USB 来做一个触控 USB 鼠标,本章我们将利 用 STM32 的 USB 来做一个 USB 读卡器。本章分为如下几个部分: 39.1 USB 读卡器简介 39.2 硬件设计 39.3 软件设计 39.4 下载验证 514 39.1 USB 读卡器简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 ALIENTEK MiniSTM32 开发板板载了一个 SD 卡插槽,可以用来接入 SD 卡,另外 MiniSTM32 开发板板载了一个 8M 字节的 SPI FLASH 芯片,通过 STM32 的 USB 接口,我们可 以实现一个简单的 USB 读卡器,来读写 SD 卡和 SPI FLASH。 本章我们还是通过移植官方的 USB Mass_Storage 例程来实现,该例程在 STSW-STM32121\ STM32_USB-FS-Device_Lib_V4.0.0\Projects\Mass_Storage 下可以找到(STSW-STM32121 是官 方的 USB 库压缩包,在光盘:8,STM32 参考资料\STM32 USB 学习资料文件夹下),注意这 里并非完全照搬官网的例程,有部分代码是被我们修改过的,以支持我们的应用。 USB Mass Storage 类支持两个传输协议: 1)Bulk-Only 传输(BOT) 2)Control/Bulk/Interrupt 传输(CBI) Mass Storage 类规范定义了两个类规定的请求:Get_Max_LUN 和 Mass Storage Reset,所有 的 Mass Storage 类设备都必须支持这两个请求。 Get_Max_LUN(bmRequestType= 10100001b and bRequest= 11111110b)用来确认设备支持 的逻辑单元数。Max LUN 的值必须是 0~15。注意:LUN 是从 0 开始的。主机不能向不存在的 LUN 发送 CBW,本章我们定义 Max LUN 的值为 1,即代表 2 个逻辑单元。 Mass Storage Reset(bmRequestType=00100001b and bRequest= 11111111b)用来复位 Mass Storage 设备及其相关接口。 支持 BOT 传输的 Mass Storage 设备接口描述符要求如下: 接口类代码 bInterfaceClass=08h,表示为 Mass Storage 设备; 接口类子代码 bInterfaceSubClass=06h,表示设备支持 SCSI Primary Command-2(SPC-2); 协议代码 bInterfaceProtocol 有 3 种:0x00、0x01、0x50,前两种需要使用中断传输,最后 一种仅使用批量传输(BOT)。 支持 BOT 的设备必须支持最少 3 个 endpoint:Control, Bulk-In 和 Bulk-Out。USB2.0 的规 范定义了控制端点 0。Bulk-In 端点用来从设备向主机传送数据(本章用端点 1 实现)。Bulk-Out 端点用来从主机向设备传送数据(本章用端点 2 实现)。 ST 官方的例程是通过 USB 来读写 SD 卡(SDIO 方式)和 NAND FALSH,支持 2 个逻辑 单元,我们在官方例程的基础上,只需要修改 SD 驱动部分代码(改为 SPI),并将对 NAND FLASH 的操作修改为对 SPI FLASH 的操作。只要这两步完成了,剩下的就比较简单了,对底 层磁盘的读写,都是在 mass_mal.c 文件实现的,所以我们只需要修改该函数的 MAL_Init、 MAL_Write、MAL_Read 和 MAL_GetStatus 等 4 个函数,使之与我们的 SD 卡和 SPI FLASH 对 应起来即可。 本章我对 SD 卡和 SPI FLASH 的操作都是采用 SPI 方式,所以速度相对 SDIO 和 FSMC 控 制的 NAND FLASH 来说,相对会慢一些。 39.2 硬件设计 本节实验功能简介:开机的时候先检测 SD 卡和 SPI FLASH 是否存在,如果存在则获取其 容量,并显示在 LCD 上面(如果不存在,则报错)。之后开始 USB 配置,在配置成功之后就 可以在电脑上发现两个可移动磁盘。我们用 DS1 来指示 USB 正在读写 SD 卡,并在液晶上显 示出来,同样我们还是用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 、DS1 515 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 2) 串口 3) TFTLCD 模块 4) SD 卡 5) SPI FLASH 6) USB 接口 这几个部分,在之前的实例中都已经介绍过了,我们在此就不多说了。不过还是要注意一 下 P13 的连接,要和上一章一样! 39.3 软件设计 打开本实验的工程文件夹目录可以看到,我们在 USB 文件夹下面新建 LIB 和 CONFIG 文 件夹,分别用来存放与 USB 核相关的代码以及配置部分代码。这两部分代码我们也不细说(详 见光盘本例程源码),其中 USB 文件夹里面的代码同上一章的一模一样,而 CONFIG 文件夹里 面的源码则来自:STSW-STM32121\STM32_USB-FS-Device_Lib_V4.0.0\Projects\ Mass_Storage 文件夹下的 inc 和 src 文件夹(STSW-STM32121 由光盘:8,STM32 参考资料\STM32 USB 学习 资料,文件名:STSW-STM32121.zip 解压而来)。 打开 main.c,内容如下: //设置 USB 连接/断线 //设置 USB 连接/断线 //enable:0,断开 // 1,允许连接 void usb_port_set(u8 enable) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); if(enable)_SetCNTR(_GetCNTR()&(~(1<<1)));//退出断电模式 else { _SetCNTR(_GetCNTR()|(1<<1)); // 断电模式 GPIOA->CRH&=0XFFF00FFF; GPIOA->CRH|=0X00033000; PAout(12)=0; } } int main(void) { u8 offline_cnt=0; u8 tct=0; u8 USB_STA; u8 Divece_STA; NVIC_Configuration(); delay_init(); //延时函数初始化 uart_init(9600); //串口初始化为 9600 LCD_Init(); //初始化液晶 LED_Init(); //LED 初始化 516 STM32 不完全手册(库函数版) KEY_Init(); //按键初始化 POINT_COLOR=RED; //设置字体为红色 ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_ShowString(60,50,200,16,16,"Mini STM32"); LCD_ShowString(60,70,200,16,16,"USB Card Reader TEST"); LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(60,110,200,16,16,"2014/3/15"); SPI_Flash_Init(); if(SD_Initialize())LCD_ShowString(60,130,200,16,16,"SD Card Error!"); //SD 卡错误 else //SD 卡正常 { LCD_ShowString(60,130,200,16,16,"SD Card Size: MB"); Mass_Memory_Size[0]=(long long)SD_GetSectorCount()*512; //得到 SD 卡容量(字节),当 SD 卡容量超过 4G 时,需要用到两个 64bit 来表示 Mass_Block_Size[0] =512;// Block 大小为 512 个字节. Mass_Block_Count[0]=Mass_Memory_Size[0]/Mass_Block_Size[0]; LCD_ShowNum(164,130,Mass_Memory_Size[0]>>20,5,16); //显示 SD 卡容量 } if(SPI_FLASH_TYPE!=W25Q64)LCD_ShowString(60,130,200,16,16,"W25Q64 Error!"); else //SPI FLASH 正常 { Mass_Memory_Size[1]=4916*1024;//前 4.8M 字节 Mass_Block_Size[1] =512;//Block 大小为 512 个字节. Mass_Block_Count[1]=Mass_Memory_Size[1]/Mass_Block_Size[1]; LCD_ShowString(60,150,200,16,16,"SPI FLASH Size:4916KB"); } delay_ms(1800); usb_port_set(0); delay_ms(300); //USB 先断开 usb_port_set(1); //USB 再次连接 LCD_ShowString(60,170,200,16,16,"USB Connecting..."); //提示 SD 卡已经准备了 Data_Buffer=mymalloc(BULK_MAX_PACKET_SIZE*2*4); //为 USB 缓存区申请内存 Bulk_Data_Buff=mymalloc(BULK_MAX_PACKET_SIZE); //申请内存 USB_Interrupts_Config(); //USB 中断配置 Set_USBClock(); //USB 时钟设置 USB_Init(); //USB 初始化 delay_ms(1800); while(1) { delay_ms(1); if(USB_STA!=USB_STATUS_REG)//状态改变了 { LCD_Fill(60,190,240,190+16,WHITE);//清除显示 if(USB_STATUS_REG&0x01)//正在写 { 517 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_ShowString(60,190,200,16,16,"USB Writing...");//USB 正在写数据 } if(USB_STATUS_REG&0x02)//正在读 { LCD_ShowString(60,190,200,16,16,"USB Reading...");//USB 正在读数据 } if(USB_STATUS_REG&0x04)LCD_ShowString(60,210,200,16,16,"USB Write Err ");//提示写入错误 else LCD_Fill(60,210,240,210+16,WHITE);//清除显示 if(USB_STATUS_REG&0x08)LCD_ShowString(60,230,200,16,16,"USB Read Err ");//提示读出错误 else LCD_Fill(60,230,240,230+16,WHITE);//清除显示 USB_STA=USB_STATUS_REG;//记录最后的状态 } if(Divece_STA!=bDeviceState) { if(bDeviceState==CONFIGURED)LCD_ShowString (60,170,200,16,16,"USB Connected ");//提示 USB 连接已经建立 else LCD_ShowString(60,170,200,16,16,"USB DisConnected ");//USB 拔出了 Divece_STA=bDeviceState; } tct++; if(tct==200) { tct=0; LED0=!LED0;//提示系统在运行 if(USB_STATUS_REG&0x10) { offline_cnt=0;//USB 连接了,则清除 offline 计数器 bDeviceState=CONFIGURED; }else//没有得到轮询 { offline_cnt++; if(offline_cnt>10)bDeviceState=UNCONNECTED; //2s 内没收到在线标记,代表 USB 被拔出了 } USB_STATUS_REG=0; } }; } 此部分代码除了 main 函数,还有一个 usb_port_set 函数,usb_port_set 函数我们在上一章已 经介绍过了,这里就不多说。我们将 SPI FLASH 的最开始 4.8M 地址范围用作 SPI FLASH Disk, 也就是文件系统管理的范围大小,这个我们在之前的 SPI FLASH 也介绍过。 518 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 通过此部分代码就可以实现了我们之前在硬件设计部分描述的功能,这里我们用到了一个 全局变量 Usb_Status_Reg,用来标记 USB 的相关状态,这样我们就可以在液晶上显示当前 USB 的状态了。 软件设计部分就为大家介绍到这里。 39.4 下载验证 在代码编译成功之后,我们通过下载代码到 MiniSTM32 开发板上,在 USB 配置成功后(假 设已经插入 SD 卡,注意:USB 数据线,要插在开发板侧面的 USB 口!不是 USB_232 端口!), LCD 显示效果如图 39.4.1 所示: 图 39.4.1 USB 连接成功 此时,电脑提示发现新硬件如图 39.4.2 所示: 图 39.4.2 USB 读卡器被电脑找到 等 USB 配置成功后,DS1 不亮,DS0 闪烁,并且在电脑上可以看到我们的磁盘,如图 39.4.3 所示: 519 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 39.4.3 电脑找到 USB 读卡器的两个盘符 我们打开设备管理器,在通用串行总线控制器里面可以发现多出了一个 USB Mass Storage Device,同时看到磁盘驱动器里面多了 2 个磁盘,如图 39.4.4 所示: 图 39.4.4 通过设备管理器查看磁盘驱动器 此时,我们就可以通过电脑读写 SD 卡或者 SPI FLASH 里面的内容了。在执行读写操作的 时候,就可以看到 DS1 亮,并且会在液晶上显示当前的读写状态。 注意,在对 SPI FLASH 操作的时候,最好不要频繁的往里面写数据,否则很容易将 SPI FLASH 写爆!! 520 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第四十章 UCOSII 实验 1-任务调度 前面我们所有的例程都是跑的裸机程序(裸奔),从本章开始,我们将分 3 个章节向大家介 绍 UCOSII(实时多任务操作系统内核)的使用。本章,我们将向大家介绍 UCOSII 最基本也是 最重要的应用:任务调度。本章分为如下几个部分: 40.1 UCOSII 简介 40.2 硬件设计 40.3 软件设计 40.4 下载验证 521 40.1 UCOSII 简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 UCOSII 的前身是 UCOS,最早出自于 1992 年美国嵌入式系统专家 Jean J.Labrosse 在《嵌 入式系统编程》杂志的 5 月和 6 月刊上刊登的文章连载,并把 UCOS 的源码发布在该杂志的 BBS 上。目前最新的版本:UCOSIII 已经出来,但是现在使用最为广泛的还是 UCOSII,本章 我们主要针对 UCOSII 进行介绍。 UCOSII 是一个可以基于 ROM 运行的、可裁减的、抢占式、实时多任务内核,具有高度可 移植性,特别适合于微处理器和控制器,是和很多商业操作系统性能相当的实时操作系统 (RTOS)。为了提供最好的移植性能,UCOSII 最大程度上使用 ANSI C 语言进行开发,并且已 经移植到近 40 多种处理器体系上,涵盖了从 8 位到 64 位各种 CPU(包括 DSP)。 UCOSII 是专门为计算机的嵌入式应用设计的, 绝大部分代码是用 C 语言编写的。CPU 硬 件相关部分是用汇编语言编写的、总量约 200 行的汇编语言部分被压缩到最低限度,为的是便 于移植到任何一种其它的 CPU 上。用户只要有标准的 ANSI 的 C 交叉编译器,有汇编器、连 接器等软件工具,就可以将 UCOSII 嵌人到开发的产品中。UCOSII 具有执行效率高、占用空间 小、实时性能优良和可扩展性强等特点, 最小内核可编译至 2KB 。UCOSII 已经移植到了几 乎所有知名的 CPU 上。 UCOSII 构思巧妙。结构简洁精练,可读性强,同时又具备了实时操作系统的全部功能, 虽然它只是一个内核,但非常适合初次接触嵌入式实时操作系统的朋友,可以说是麻雀虽小, 五脏俱全。UCOSII(V2.91 版本)体系结构如图 40.1.1 所示: 用户应用程序 UCOSII UCOSII与处理器无关的代码 ucos_ii.h ucos_ii.c os_tmr.c os_time.c os_task.c os_sem.c os_q.c os_mutex.c os_mem.c os_mbox.c os_flag.c os_core.c UCOSII与应用程序 相关代码 os_cfg.h includes.h UCOSII与处理器相关的代码(移植时需要修改) os_cpu.h os_cpu_a.asm os_cpu_c.c CPU 定时器 图 40.1.1 UCOSII 体系结构图 注意本章我们使用的是:UCOSII V2.91 版本,该版本 UCOSII 比早期的 UCOSI(I 如 V2.52) 多了很多功能(比如多了软件定时器,支持任务数最大达到 255 个等),而且修正了很多已知 BUG。不过,有两个文件:os_dbg_r.c 和 os_dbg.c,我们没有在上图列出,也不将其加入到我 们的工程中,这两个主要用于对 UCOS 内核进行调试支持,比较少用到。 从上图可以看出,UCOSII 的移植,我们只需要修改:os_cpu.h、os_cpu_a.asm 和 os_cpu.c 等三个文件即可,其中:os_cpu.h,进行数据类型的定义,以及处理器相关代码和几个函数原 型;os_cpu_a.asm,是移植过程中需要汇编完成的一些函数,主要就是任务切换函数;os_cpu.c, 522 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 定义一些用户 HOOK 函数。 图中定时器的作用是为 UCOSII 提供系统时钟节拍,实现任务切换和任务延时等功能。这 个时钟节拍由 OS_TICKS_PER_SEC(在 os_cfg.h 中定义)设置,一般我们设置 UCOSII 的系统 时钟节拍为 1ms~100ms,具体根据你所用处理器和使用需要来设置。本章,我们利用 STM32 的 SYSTICK 定时器来提供 UCOSII 时钟节拍。 关于 UCOSII 在 STM32 的详细移植,请参考光盘资料(《UCOSII 在 STM32 的移植详解.pdf》), 这里我们就不详细介绍了。 UCOSII 早期版本只支持 64 个任务,但是从 2.80 版本开始,支持任务数提高到 255 个,不 过对我们来说一般 64 个任务都是足够多了,一般很难用到这么多个任务。UCOSII 保留了最高 4 个优先级和最低 4 个优先级的总共 8 个任务,用于拓展使用,单实际上,UCOSII 一般只占用 了最低 2 个优先级,分别用于空闲任务(倒数第一)和统计任务(倒数第二),所以剩下给我 们使用的任务最多可达 255-2=253 个(V2.91)。 所谓的任务,其实就是一个死循环函数,该函数实现一定的功能,一个工程可以有很多这 样的任务(最多 255 个),UCOSII 对这些任务进行调度管理,让这些任务可以并发工作(注 意不是同时工作!!,并发只是各任务轮流占用 CPU,而不是同时占用,任何时候还是只有 1 个任务能够占用 CPU),这就是 UCOSII 最基本的功能。Ucos 任务的一般格式为: void MyTask (void *pdata) { 任务准备工作… While(1)//死循环 { 任务 MyTask 实体代码; OSTimeDlyHMSM(x,x,x,x);//调用任务延时函数,释放 cpu 控制权, } } 假如我们新建了 2 个任务为 MyTask 和 YourTask,这里我们先忽略任务优先级的概念,两个 任务死循环中延时时间为 1s。如果某个时刻,任务 MyTask 在执行中,当它执行到延时函数 OSTimeDlyHMSM 的时候,它释放 cpu 控制权,这个时候,任务 YourTask 获得 cpu 控制权开 始执行,任务 YourTask 执行过程中,也会调用延时函数延时 1s 释放 CPU 控制权,这个过程中 任务 A 延时 1s 到达,重新获得 CPU 控制权,重新开始执行死循环中的任务实体代码。如此循 环,现象就是两个任务交替运行,就好像 CPU 在同时做两件事情一样。 疑问来了,如果有很多任务都在等待,那么先执行那个任务呢?如果任务在执行过程中, 想停止之后去执行其他任务是否可行呢?这里就涉及到任务优先级以及任务状态任务控制的一 些知识,我们在后面会有所提到。如果要详细的学习,建议看任哲老师的《ucosII 实时操作系 统》一书。 前面我们学习的所有实验,都是一个大任务(死循环),这样,有些事情就比较不好处理, 比如:MP3 实验,在 MP3 播放的时候,我们还希望显示歌词,如果是一个死循环(一个任务), 那么很可能在显示歌词的时候,MP3 声音出现停顿(尤其是高码率的时候),这主要是歌词显 示占用太长时间,导致 VS1053 由于不能及时得到数据而停顿。而如果用 UCOSII 来处理,那 么我们可以分 2 个任务,MP3 播放一个任务(优先级高),歌词显示一个任务(优先级低)。 这样,由于 MP3 任务的优先级高于歌词显示任务,MP3 任务可以打断歌词显示任务,从而及 时给 VS1053 提供数据,保证音频不断,而显示歌词又能顺利进行。这就是 UCOSII 带来的好 处。 这里有几个 UCOSII 相关的概念需要大家了解一下。任务优先级,任务堆栈,任务控制块, 523 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 任务就绪表和任务调度器。 任务优先级,这个概念比较好理解,ucos 中,每个任务都有唯一的一个优先级。优先级是 任务的唯一标识。在 UCOSII 中,使用 CPU 的时候,优先级高(数值小)的任务比优先级低的 任务具有优先使用权,即任务就绪表中总是优先级最高的任务获得 CPU 使用权,只有高优先级 的任务让出 CPU 使用权(比如延时)时,低优先级的任务才能获得 CPU 使用权。UCOSII 不支 持多个任务优先级相同,也就是每个任务的优先级必须不一样。 任务堆栈,就是存储器中的连续存储空间。为了满足任务切换和响应中断时保存 CPU 寄存 器中的内容以及任务调用其他函数时的需要,每个任务都有自己的堆栈。在创建任务的时候, 任务堆栈是任务创建的一个重要入口参数。 任务控制块 OS_TCB,用来记录任务堆栈指针,任务当前状态以及任务优先级等任务属性。 UCOSII 的任何任务都是通过任务控制块(TCB)的东西来控制的,一旦任务创建了,任务控 制块 OS_TCB 就会被赋值。每个任务管理块有 3 个最重要的参数:1,任务函数指针;2,任务 堆栈指针;3,任务优先级;任务控制块就是任务在系统里面的身份证(UCOSII 通过优先级识 别任务),任务控制块我们就不再详细介绍了,详细介绍请参考任哲老师的《嵌入式实时操作 系统 UCOSII 原理及应用》一书第二章。 任务就绪表,简而言之就是用来记录系统中所有处于就绪状态的任务。它是一个位图,系 统中每个任务都在这个位图中占据一个进制位,该位置的状态(1 或者 0)就表示任务是否处于 就绪状态。 任务调度的作用一是在任务就绪表中查找优先级最高的就绪任务,二是实现任务的切换。 比如说,当一个任务释放 cpu 控制权后,进行一次任务调度,这个时候任务调度器首先要去任 务就绪表查询优先级最高的就绪任务,查到之后,进行一次任务切换,转而去执行下一个任务。 关于任务调度的详细介绍,请参考《嵌入式实时操作系统 UCOSII 原理及应用》一书第三章相 关内容。 UCOSII 的每个任务都是一个死循环。每个任务都处在以下 5 种状态之一的状态下,这 5 种状态是:睡眠状态、 就绪状态、 运行状态、 等待状态(等待某一事件发生)和中断服务状态。 睡眠状态,任务在没有被配备任务控制块或被剥夺了任务控制块时的状态。 就绪状态,系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记,任务已经准 备好了,但由于该任务的优先级比正在运行的任务的优先级低, 还暂时不能运行,这时任务的 状态叫做就绪状态。 运行状态,该任务获得 CPU 使用权,并正在运行中,此时的任务状态叫做运行状态。 等待状态,正在运行的任务,需要等待一段时间或需要等待一个事件发生再运行时,该任 务就会把 CPU 的使用权让给别的任务而使任务进入等待状态。 中断服务状态,一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程 序,这时任务的状态叫做中断服务状态。 UCOSII 任务的 5 个状态转换关系如图 58.1.2 所示: 524 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 58.1.2 UCOSII 任务状态转换关系 接下来,我们看看在 UCOSII 中,与任务相关的几个函数: 1) 建立任务函数 如果想让 UCOSII 管理用户的任务,必须先建立任务。UCOSII 提供了我们 2 个建立任 务的函数:OSTaskCreat 和 OSTaskCreatExt,我们一般用 OSTaskCreat 函数来创建任务, 该函数原型为: OSTaskCreate(void(*task)(void*pd),void*pdata,OS_STK*ptos,INTU prio); 该函数包括 4 个参数:task:是指向任务代码的指针;pdata:是任务开始执行时,传 递给任务的参数的指针;ptos:是分配给任务的堆栈的栈顶指针;prio 是分配给任务的优 先级。 每个任务都有自己的堆栈,堆栈必须申明为 OS_STK 类型,并且由连续的内存空间组 成。可以静态分配堆栈空间,也可以动态分配堆栈空间。 OSTaskCreatExt 也可以用来创建任务,详细介绍请参考《嵌入式实时操作系统 UCOSII 原理及应用》3.5.2 节。 2) 任务删除函数 所谓的任务删除,其实就是把任务置于睡眠状态,并不是把任务代码给删除了。UCOSII 提供的任务删除函数原型为: INT8U OSTaskDel(INT8U prio); 其中参数 prio 就是我们要删除的任务的优先级,可见该函数是通过任务优先级来实现 任务删除的。 特别注意:任务不能随便删除,必须在确保被删除任务的资源被释放的前提下才能删 除! 3) 请求任务删除函数 前面提到,必须确保被删除任务的资源被释放的前提下才能将其删除,所以我们通过 向被删除任务发送删除请求,来实现任务释放自身占用资源后再删除。UCOSII 提供的请 求删除任务函数原型为: INT8U OSTaskDelReq(INT8U prio); 525 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 同样还是通过优先级来确定被请求删除任务。 4) 改变任务的优先级函数 UCOSII 在建立任务时,会分配给任务一个优先级,但是这个优先级并不是一成不变的, 而是可以通过调用 UCOSII 提供的函数修改。UCOSII 提供的任务优先级修改函数原型为: INT8U OSTaskChangePrio(INT8U oldprio,INT8U newprio); 5) 任务挂起函数 任务挂起和任务删除有点类似,但是又有区别,任务挂起只是将被挂起任务的就绪标 志删除,并做任务挂起记录,并没有将任务控制块任务控制块链表里面删除,也不需要释 放其资源,而任务删除则必须先释放被删除任务的资源,并将被删除任务的任务控制块也 给删了。被挂起的任务,在恢复(解挂)后可以继续运行。UCOSII 提供的任务挂起函数 原型为: INT8U OSTaskSuspend(INT8U prio); 6) 任务恢复函数 有任务挂起函数,就有任务恢复函数,通过该函数将被挂起的任务恢复,让调度器能 够重新调度该函数。UCOSII 提供的任务恢复函数原型为: INT8U OSTaskResume(INT8U prio); UCOSII 与任务相关的函数我们就介绍这么多。最后,我们来看看在 STM32 上面运行 UCOSII 的步骤: 1) 移植 UCOSII 要想 UCOSII 在 STM32 正常运行,当然首先是需要移植 UCOSII,这部分我们已经为大 家做好了(参考光盘源码,想自己移植的,请参考光盘 UCOSII 资料)。 这里我们要特别注意一个地方,ALIENTEK 提供的 SYSTEM 文件夹里面的系统函数直 接支持 UCOSII,只需要在 sys.h 文件里面将:SYSTEM_SUPPORT_UCOS 宏定义改为 1, 即可通过 delay_init 函数初始化 UCOSII 的系统时钟节拍,为 UCOSII 提供时钟节拍。 2) 编写任务函数并设置其堆栈大小和优先级等参数。 编写任务函数,以便 UCOSII 调用。 设置函数堆栈大小,这个需要根据函数的需求来设置,如果任务函数的局部变量多,嵌 套层数多,那么相应的堆栈就得大一些,如果堆栈设置小了,很可能出现的结果就是 CPU 进入 HardFault,遇到这种情况,你就必须把堆栈设置大一点了。另外,有些地方还需要注 意堆栈字节对齐的问题,如果任务运行出现莫名其妙的错误(比如用到 sprintf 出错),请 考虑是不是字节对齐的问题。 设置任务优先级,这个需要大家根据任务的重要性和实时性设置,记住高优先级的任务 有优先使用 CPU 的权利。 3) 初始化 UCOSII,并在 UCOSII 中创建任务 调用 OSInit,初始化 UCOSII,通过调用 OSTaskCreate 函数创建我们的任务。 4) 启动 UCOSII 调用 OSStart,启动 UCOSII。 通过以上 4 个步骤,UCOSII 就开始在 STM32 上面运行了,这里还需要注意我们必须对 os_cfg.h 进行部分配置,以满足我们自己的需要。 40.2 硬件设计 本节实验功能简介:本章我们在 UCOSII 里面创建 3 个任务:开始任务、LED0 任务和 LED1 任务,开始任务用于创建其他(LED0 和 LED1)任务,之后挂起;LED0 任务用于控制 DS0 526 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 的亮灭,DS0 每秒钟亮 80ms;LED1 任务用于控制 DS1 的亮灭,DS1 亮 300ms,灭 300ms, 依次循环。 所要用到的硬件资源如下: 1) 指示灯 DS0 、DS1 40.3 软件设计 本章,我们在第六章实验 (实验 1 )的基础上修改,在该工程源码下面加入 UCOSII 文 件夹,存放 UCOSII 源码(我们已经将 UCOSII 源码分为三个文件夹:CORE、PORT 和 CONFIG)。 打开工程,新建 UCOSII-CORE、UCOSII-PORT 和 UCOSII-CONFIG 三个分组,分别添加 UCOSII 三个文件夹下的源码,并将这三个文件夹加入头文件包含路径,最后得到工程如图 40.3.1 所示: 图 40.3.1 添加 UCOSII 源码后的工程 UCOSII-CORE 分组下面是 UCOSII 的核心源码,我们不需要做任何变动。 UCOSII-PORT 分组下面是我们移植 UCOSII 要修改的 3 个代码,这个在移植的时候完成。 UCOSII-CONFIG 分组下面是 UCOSII 的配置部分,主要由用户根据自己的需要对 UCOSII 进行裁剪或其他设置。 本章,我们对 os_cfg.h 里面定义 OS_TICKS_PER_SEC 的值为 200,也就是设置 UCOSII 的时钟节拍为 5ms,同时设置 OS_MAX_TASKS 为 10,也就是最多 10 个任务(包括空闲任务 和统计任务在内),其他配置我们就不详细介绍了,请参考本实验源码。 前面提到,我们需要在 sys.h 里面设置 SYSTEM_SUPPORT_UCOS 为 1,以支持 UCOSII, 通过这个设置,我们不仅可以实现利用 delay_init 来初始化 SYSTICK,产生 UCOSII 的系统时 钟节拍,还可以让 delay_us 和 delay_ms 函数在 UCOSII 下能够正常使用(实现原理请参考 5.1 527 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 节),这使得我们之前的代码,可以十分方便的移植到 UCOSII 下。虽然 UCOSII 也提供了延时 函数:OSTimeDly 和 OSTimeDLyHMSM,但是这两个函数的最少延时单位只能是 1 个 UCOSII 时钟 节拍,在本章,即 5ms,显然不能实现 us 级的延时,而 us 级的延时在很多时候非常有用:比 如 IIC 模拟时序,DS18B20 等单总线器件操作等。而通过我们提供的 delay_us 和 delay_ms,则 可以方便的提供 us 和 ms 的延时服务,这比 UCOSII 本身提供的延时函数更好用。 在设置 SYSTEM_SUPPORT_UCOS 为 1 之后,UCOSII 的时钟节拍由 SYSTICK 的中断服 务函数提供,该部分代码如下: //systick 中断服务函数,使用 ucos 时用到 void SysTick_Handler(void) { OSIntEnter(); OSTimeTick(); OSIntExit(); //进入中断 //调用 ucos 的时钟服务程序 //触发任务切换软中断 } 以上代码,其中 OSIntEnter 是进入中断服务函数,用来记录中断嵌套层数(OSIntNesting 增加 1);OSTimeTick 是系统时钟节拍服务函数,在每个时钟节拍了解每个任务的延时状态, 使已经到达延时时限的非挂起任务进入就绪状态;OSIntExit 是退出中断服务函数,该函数可能 触发一次任务切换(当 OSIntNesting==0&&调度器未上锁&&就绪表最高优先级任务!=被中断 的任务优先级时),否则继续返回原来的任务执行代码(如果 OSIntNesting 不为 0,则减 1)。 事实上,任何中断服务函数,我们都应该加上 OSIntEnter 和 OSIntExit 函数,这是因为 UCOSII 是一个可剥夺型的内核,中断服务子程序运行之后,系统会根据情况进行一次任务调 度去运行优先级别最高的就绪任务,而并不一定接着运行被中断的任务! 最后,我们打开 main.c,代码如下: /////////////////////////UCOSII 任务设置/////////////////////////////////// //START 任务 //设置任务优先级 #define START_TASK_PRIO 10 //开始任务的优先级设置为最低 //设置任务堆栈大小 #define START_STK_SIZE 64 //任务堆栈 OS_STK START_TASK_STK[START_STK_SIZE]; //任务函数 void start_task(void *pdata); //LED0 任务 //设置任务优先级 #define LED0_TASK_PRIO 7 //设置任务堆栈大小 #define LED0_STK_SIZE 64 //任务堆栈 OS_STK LED0_TASK_STK[LED0_STK_SIZE]; //任务函数 void led0_task(void *pdata); 528 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 //LED1 任务 //设置任务优先级 #define LED1_TASK_PRIO 6 //设置任务堆栈大小 #define LED1_STK_SIZE 64 //任务堆栈 OS_STK LED1_TASK_STK[LED1_STK_SIZE]; //任务函数 void led1_task(void *pdata); int main(void) { delay_init(); //延时函数初始化 NVIC_Configuration(); LED_Init(); //初始化与 LED 连接的硬件接口 OSInit(); OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE -1],START_TASK_PRIO );//创建起始任务 OSStart(); } //开始任务 void start_task(void *pdata) { OS_CPU_SR cpu_sr=0; pdata = pdata; OS_ENTER_CRITICAL(); //进入临界区(无法被中断打断) OSTaskCreate(led0_task,(void *)0,(OS_STK*)&LED0_TASK_STK[LED0_STK_SIZE-1], LED0_TASK_PRIO); OSTaskCreate(led1_task,(void *)0,(OS_STK*)&LED1_TASK_STK[LED1_STK_SIZE-1], LED1_TASK_PRIO); OSTaskSuspend(START_TASK_PRIO); //挂起起始任务. OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断) } //LED0 任务 void led0_task(void *pdata) { while(1) { LED0=0; delay_ms(80); LED0=1; delay_ms(920); }; } //LED1 任务 529 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 void led1_task(void *pdata) { while(1) { LED1=0; delay_ms(300); LED1=1; delay_ms(300); }; } 该部分代码我们创建了 3 个任务:start_task、led0_task 和 led1_task,优先级分别是 10、7 和 6,堆栈大小都是 64(注意 OS_STK 为 32 位数据)。我们在 main 函数只创建了 start_task 一 个任务,然后在 start_task 再创建另外两个任务,在创建之后将自身(start_task)挂起。这里, 我们单独创建 start_task,是为了提供一个单一任务,实现应用程序开始运行之前的准备工作(比 如:外设初始化、创建信号量、创建邮箱、创建消息队列、创建信号量集、创建任务、初始化 统计任务等等)。 在应用程序中经常有一些代码段必须不受任何干扰地连续运行,这样的代码段叫做临界段 (或临界区)。因此,为了使临界段在运行时不受中断所打断,在临界段代码前必须用关中断指 令使 CPU 屏蔽中断请求,而在临界段代码后必须用开中断指令解除屏蔽使得 CPU 可以响应中 断请求。UCOSII 提供 OS_ENTER_CRITICAL 和 OS_EXIT_CRITICAL 两个宏来实现,这两个 宏需要我们在移植 UCOSII 的时候实现,本章我们采用方法 3(即 OS_CRITICAL_METHOD 为 3)来实现这两个宏。因为临界段代码不能被中断打断,将严重影响系统的实时性,所以临界段 代码越短越好! 在 start_task 任务中,我们在创建 led0_task 和 led1_task 的时候,不希望中断打断,故使用 了临界区。其他两个任务,就十分简单了,我们就不细说了,注意我们这里使用的延时函数还 是 delay_ms,而不是直接使用的 OSTimeDly。 另外,一个任务里面一般是必须有延时函数的,以释放 CPU 使用权,否则可能导致低优先 级的任务因高优先级的任务不释放 CPU 使用权而一直无法得到 CPU 使用权,从而无法运行。 软件设计部分就为大家介绍到这里。 40.4 下载验证 在代码编译成功之后,我们通过下载代码到 MiniSTM32 开发板上,可以看到 DS0 一秒钟 闪一次,而 DS1 则以固定的频率闪烁,说明两个任务(led0_task 和 led1_task)都已经正常运行 了,符合我们预期的设计。 530 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第四十一章 UCOSII 实验 2-信号量和邮箱 上一章,我们学习了如何使用 UCOSII,学习了 UCOSII 的任务调度,但是并没有用到任务 间的同步与通信,本章我们将学习两个最基本的任务间通讯方式:信号量和邮箱。本章分为如 下几个部分: 41.1 UCOSII 信号量和邮箱简介 41.2 硬件设计 41.3 软件设计 41.4 下载验证 531 41.1 UCOSII 信号量和邮箱简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 系统中的多个任务在运行时,经常需要互相无冲突地访问同一个共享资源,或者需要互相 支持和依赖,甚至有时还要互相加以必要的限制和制约,才保证任务的顺利运行。因此,操作 系统必须具有对任务的运行进行协调的能力,从而使任务之间可以无冲突、流畅地同步运行, 而不致导致灾难性的后果。 例如,任务 A 和任务 B 共享一台打印机,如果系统已经把打印机分配给了任务 A,则任务 B 因不能获得打印机的使用权而应该处于等待状态,只有当任务 A 把打印机释放后,系统才能 唤醒任务 B 使其获得打印机的使用权。如果这两个任务不这样做,那么会造成极大的混乱 。 任务间的同步依赖于任务间的通信。在 UCOSII 中,是使用信号量、邮箱(消息邮箱)和 消息队列这些被称作事件的中间环节来实现任务之间的通信的。本章,我们仅介绍信号量和邮 箱,消息队列将会在下一章介绍。 事件 两个任务通过事件进行通讯的示意图如图 41.1.1 所示: 任务1 发送事件 事件 请求事件 任务2 图 41.1.1 两个任务使用事件进行通信的示意图 在图 41.1.1 中任务 1 是发信方,任务 2 是收信方。任务 1 负责把信息发送到事件上,这项 操作叫做发送事件。任务 2 通过读取事件操作对事件进行查询:如果有信息则读取,否则等待。 读事件操作叫做请求事件。 为了把描述事件的数据结构统一起来,UCOSII 使用叫做事件控制块(ECB)的数据结构来描 述诸如信号量、邮箱(消息邮箱)和消息队列这些事件。事件控制块中包含包括等待任务表在 内的所有有关事件的数据,事件控制块结构体定义如下: typedef struct { INT8U OSEventType; //事件的类型 INT16U OSEventCnt; //信号量计数器 void *OSEventPtr; //消息或消息队列的指针 INT8U OSEventGrp; //等待事件的任务组 INT8U OSEventTbl[OS_EVENT_TBL_SIZE];//任务等待表 #if OS_EVENT_NAME_EN > 0u INT8U *OSEventName; //事件名 #endif } OS_EVENT; 信号量 信号量是一类事件。使用信号量的最初目的,是为了给共享资源设立一个标志,该标志表 示该共享资源的占用情况。这样,当一个任务在访问共享资源之前,就可以先对这个标志进行 查询,从而在了解资源被占用的情况之后,再来决定自己的行为。 信号量可以分为两种:一种是二值型信号量,另外一种是 N 值信号量。 二值型信号量好比家里的座机,任何时候,只能有一个人占用。而 N 值信号量,则好比公 共电话亭,可以同时有多个人(N 个)使用。 532 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 UCOSII 将二值型信号量称之为也叫互斥型信号量,将 N 值信号量称之为计数型信号量, 也就是普通的信号量。本章,我们介绍的是普通信号量,互斥型信号量的介绍,请参考《嵌入 式实时操作系统 UCOSII 原理及应用》5.4 节。 接下来我们看看在 UCOSII 中,与信号量相关的几个函数(未全部列出,下同)。 1) 创建信号量函数 在使用信号量之前,我们必须用函数 OSSemCreate 来创建一个信号量,该函数的原型 为: OS_EVENT *OSSemCreate (INT16U cnt); 该函数返回值为已创建的信号量的指针,而参数 cnt 则是信号量计数器(OSEventCnt) 的初始值。 2) 请求信号量函数 任务通过调用函数 OSSemPend 请求信号量,该函数原型如下: void OSSemPend ( OS_EVENT *pevent, INT16U timeout, INT8U *err); 其中,参数 pevent 是被请求信号量的指针,timeout 为等待时限,err 为错误信息。 为防止任务因得不到信号量而处于长期的等待状态,函数 OSSemPend 允许用参数 timeout 设置一个等待时间的限制,当任务等待的时间超过 timeout 时可以结束等待状态而 进入就绪状态。如果参数 timeout 被设置为 0,则表明任务的等待时间为无限长。 3) 发送信号量函数 任务获得信号量,并在访问共享资源结束以后,必须要释放信号量,释放信号量也叫 做发送信号量,发送信号通过 OSSemPost 函数实现 。OSSemPost 函数在对信号量的计数 器操作之前,首先要检查是否还有等待该信号量的任务。如果没有,就把信号量计数器 OSEventCnt 加一;如果有,则调用调度器 OS_Sched( )去运行等待任务中优先级别最高的 任务。函数 OSSemPost 的原型为: INT8U OSSemPost(OS_EVENT *pevent); 其中,pevent 为信号量指针,该函数在调用成功后,返回值为 OS_ON_ERR,否则会 根据具体错误返回 OS_ERR_EVENT_TYPE、OS_SEM_OVF。 4) 删除信号量函数 应用程序如果不需要某个信号量了,那么可以调用函数 OSSemDel 来删除该信号量, 该函数的原型为: OS_EVENT *OSSemDel (OS_EVENT *pevent,INT8U opt, INT8U *err); 其中,pevent 为要删除的信号量指针,opt 为删除条件选项,err 为错误信息。 邮箱 在多任务操作系统中,常常需要在任务与任务之间通过传递一个数据(这种数据叫做“消 息”)的方式来进行通信。为了达到这个目的,可以在内存中创建一个存储空间作为该数据的 缓冲区。如果把这个缓冲区称之为消息缓冲区,这样在任务间传递数据(消息)的最简单办法 就是传递消息缓冲区的指针。我们把用来传递消息缓冲区指针的数据结构叫做邮箱(消息邮箱)。 在 UCOSII 中,我们通过事件控制块的 OSEventPrt 来传递消息缓冲区指针,同时使事件控 制块的成员 OSEventType 为常数 OS_EVENT_TYPE_MBOX,则该事件控制块就叫做消息邮箱。 接下来我们看看在 UCOSII 中,与消息邮箱相关的几个函数。 1) 创建邮箱函数 创建邮箱通过函数 OSMboxCreate 实现,该函数原型为: OS_EVENT *OSMboxCreate (void *msg); 函数中的参数 msg 为消息的指针,函数的返回值为消息邮箱的指针。 533 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 调用函数 OSMboxCreate 需先定义 msg 的初始值。在一般的情况下,这个初始值为 NULL; 但 也 可以事 先定义 一个邮 箱, 然后把 这个 邮箱的 指针 作为参 数传 递到函 数 OSMboxCreate 中,使之一开始就指向一个邮箱。 2) 向邮箱发送消息函数 任务可以通过调用函数 OSMboxPost 向消息邮箱发送消息,这个函数的原型为: INT8U OSMboxPost (OS_EVENT *pevent,void *msg); 其中 pevent 为消息邮箱的指针,msg 为消息指针。 3) 请求邮箱函数 当一个任务请求邮箱时需要调用函数 OSMboxPend,这个函数的主要作用就是查看邮 箱指针 OSEventPtr 是否为 NULL,如果不是 NULL 就把邮箱中的消息指针返回给调用函数 的任务,同时用 OS_NO_ERR 通过函数的参数 err 通知任务获取消息成功;如果邮箱指针 OSEventPtr 是 NULL,则使任务进入等待状态,并引发一次任务调度。 函数 OSMboxPend 的原型为: void *OSMboxPend (OS_EVENT *pevent, INT16U timeout, INT8U *err); 其中 pevent 为请求邮箱指针,timeout 为等待时限,err 为错误信息。 4) 查询邮箱状态函数 任务可以通过调用函数 OSMboxQuery 查询邮箱的当前状态。该函数原型为: INT8U OSMboxQuery(OS_EVENT *pevent,OS_MBOX_DATA *pdata); 其中 pevent 为消息邮箱指针,pdata 为存放邮箱信息的结构。 5) 删除邮箱函数 在邮箱不再使用的时候,我们可以通过调用函数 OSMboxDel 来删除一个邮箱,该函 数原型为: OS_EVENT *OSMboxDel(OS_EVENT *pevent,INT8U opt,INT8U *err); 其中 pevent 为消息邮箱指针,opt 为删除选项,err 为错误信息。 关于 UCOSII 信号量和邮箱的介绍,就到这里。更详细的介绍,请参考《嵌入式实时操作 系统 UCOSII 原理及应用》第五章。 41.2 硬件设计 本节实验功能简介:本章我们在 UCOSII 里面创建 6 个任务(不含统计任务和空闲任务): 开始任务、LED0 任务、LED1 任务、触摸屏任务、主任务和按键扫描任务,开始任务用于创建 信号量、创建邮箱、初始化统计任务以及其他任务的创建,之后挂起;LED0 任务用于 DS0 控 制,提示程序运行状况;LED1 任务用于测试信号量,通过请求信号量函数,每得到一个信号 量,DS1 就亮一下;触摸屏任务用于在屏幕上画图,可以用于测试 CPU 使用率;按键扫描任 务用于按键扫描,优先级最高,将得到的键值通过消息邮箱发送出去;主任务则通过查询消息 邮箱获得键值,并根据键值执行信号量发送(DS1 控制)、触摸区域清屏和触摸屏校准等控制。 所要用到的硬件资源如下: 1) 指示灯 DS0 、DS1 2) 三个按键(KEY0/KEY1/WK_UP) 3) TFTLCD 模块 这些,我们在前面的学习中都已经介绍过了。 41.3 软件设计 本章,我们在第二十六章实验 (实验 21 )的基础上修改。首先,是 UCOSII 代码的添加, 534 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 具体方法同上一章一模一样,本章就不再详细介绍了。不过,本章我们将 OS_TICKS_PER_SEC 设置为 500,即 UCOSII 的时钟节拍为 2ms。 在加入 UCOSII 代码后,我们只需要修改 main.c 了,打开 main.c,代码如下: /////////////////////////UCOSII 任务设置/////////////////////////////////// //START 任务 #define START_TASK_PRIO 10 //设置任务优先级 #define START_STK_SIZE 64 //设置任务堆栈大小 OS_STK START_TASK_STK[START_STK_SIZE]; //任务堆栈 void start_task(void *pdata); //任务函数 //LED0 任务 #define LED0_TASK_PRIO 7 //设置任务优先级 #define LED0_STK_SIZE 64 //设置任务堆栈大小 OS_STK LED0_TASK_STK[LED0_STK_SIZE]; //任务堆栈 void led0_task(void *pdata); //任务函数 //触摸屏任务 #define TOUCH_TASK_PRIO 6 //设置任务优先级 #define TOUCH_STK_SIZE 64 //设置任务堆栈大小 OS_STK TOUCH_TASK_STK[TOUCH_STK_SIZE];//任务堆栈 void touch_task(void *pdata); //任务函数 //LED1 任务 #define LED1_TASK_PRIO 5 //设置任务优先级 #define LED1_STK_SIZE 64 //设置任务堆栈大小 OS_STK LED1_TASK_STK[LED1_STK_SIZE]; //任务堆栈 void led1_task(void *pdata); //任务函数 //主任务 #define MAIN_TASK_PRIO 4 //设置任务优先级 #define MAIN_STK_SIZE 128 //设置任务堆栈大小 OS_STK MAIN_TASK_STK[MAIN_STK_SIZE]; //任务堆栈 void main_task(void *pdata); //任务函数 //按键扫描任务 #define KEY_TASK_PRIO 3 //设置任务优先级 #define KEY_STK_SIZE 64 //设置任务堆栈大小 OS_STK KEY_TASK_STK[KEY_STK_SIZE]; //任务堆栈 void key_task(void *pdata); //任务函数 OS_EVENT * msg_key; //按键邮箱事件块指针 OS_EVENT * sem_led1; //LED1 信号量指针 //加载主界面 void ucos_load_main_ui(void) { LCD_Clear(WHITE); //清屏 POINT_COLOR=RED; //设置字体为红色 LCD_ShowString(30,10,200,16,16,"Mini STM32"); LCD_ShowString(30,30,200,16,16,"UCOSII TEST2"); 535 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 LCD_ShowString(30,50,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,75,200,16,16,"KEY0:LED1 KEY_UP:ADJUST"); LCD_ShowString(30,95,200,16,16,"KEY1:CLEAR"); LCD_ShowString(80,210,200,16,16,"Touch Area"); LCD_DrawLine(0,120,lcddev.width,120); LCD_DrawLine(0,70,lcddev.width,70); LCD_DrawLine(150,0,150,70); POINT_COLOR=BLUE;//设置字体为蓝色 LCD_ShowString(160,30,200,16,16,"CPU: %"); LCD_ShowString(160,50,200,16,16,"SEM:000"); } int main(void) { delay_init(); //延时函数初始化 uart_init(72); NVIC_Configuration(); LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 tp_dev.init(); //触摸屏初始化 ucos_load_main_ui(); //加载主界面 OSInit(); //初始化 UCOSII OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK[START_STK_SIZE -1],START_TASK_PRIO );//创建起始任务 OSStart(); } //开始任务 void start_task(void *pdata) { OS_CPU_SR cpu_sr=0; pdata = pdata; msg_key=OSMboxCreate((void*)0); sem_led1=OSSemCreate(0); OSStatInit(); OS_ENTER_CRITICAL(); //创建消息邮箱 //创建信号量 //初始化统计任务.这里会延时 1 秒钟左右 //进入临界区(无法被中断打断) OSTaskCreate(led0_task,(void *)0,(OS_STK*)&LED0_TASK_STK[LED0_STK_SIZE-1] ,LED0_TASK_PRIO); OSTaskCreate(touch_task,(void *)0,(OS_STK*)&TOUCH_TASK_STK [TOUCH_STK_SIZE-1],TOUCH_TASK_PRIO); OSTaskCreate(led1_task,(void *)0,(OS_STK*)&LED1_TASK_STK [LED1_STK_SIZE-1],LED1_TASK_PRIO); OSTaskCreate(main_task,(void *)0,(OS_STK*)&MAIN_TASK_STK [MAIN_STK_SIZE-1],MAIN_TASK_PRIO); 536 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 OSTaskCreate(key_task,(void *)0,(OS_STK*)&KEY_TASK_STK[KEY_STK_SIZE-1], KEY_TASK_PRIO); OSTaskSuspend(START_TASK_PRIO); OS_EXIT_CRITICAL(); //挂起起始任务. //退出临界区(可以被中断打断) } //LED0 任务 void led0_task(void *pdata) { u8 t; while(1) { t++; delay_ms(10); if(t==8)LED0=1; //LED0 灭 if(t==100) { t=0; LED0=0;} //LED0 亮 } } //LED1 任务 void led1_task(void *pdata) { u8 err; while(1) { OSSemPend(sem_led1,0,&err); LED1=0;delay_ms(200); LED1=1; delay_ms(800); } } //触摸屏任务 void touch_task(void *pdata) { while(1) { tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) //触摸屏被按下 { if(tp_dev.x[0]120) { TP_Draw_Big_Point(tp_dev.x[0],tp_dev.y[0],RED); //画图 delay_ms(2); } }else delay_ms(10); //没有按键按下的时候 } } 537 STM32 不完全手册(库函数版) //主任务 ALIENTEK MiniSTM32 V3.0 开发板教程 void main_task(void *pdata) { u32 key=0; u8 err; u8 semmask=0;u8 tcnt=0; while(1) { key=(u32)OSMboxPend(msg_key,10,&err); switch(key) { case KEY0_PRES://发送信号量 semmask=1; OSSemPost(sem_led1); break; case KEY1_PRES://清除 LCD_Fill(0,121,lcddev.width,lcddev.height,WHITE); break; case WKUP_PRES://校准 OSTaskSuspend(TOUCH_TASK_PRIO); //挂起触摸屏任务 if((tp_dev.touchtype&0X80)==0)TP_Adjust(); OSTaskResume(TOUCH_TASK_PRIO); //解挂 ucos_load_main_ui(); //重新加载主界面 break; } if(semmask||sem_led1->OSEventCnt)//需要显示 sem { POINT_COLOR=BLUE; LCD_ShowxNum(192,50,sem_led1->OSEventCnt,3,16,0X80);//显示信号量值 if(sem_led1->OSEventCnt==0)semmask=0; //停止更新 } if(tcnt==50)//0.5 秒更新一次 CPU 使用率 { tcnt=0; POINT_COLOR=BLUE; LCD_ShowxNum(192,30,OSCPUUsage,3,16,0); //显示 CPU 使用率 } tcnt++; delay_ms(10); } } //按键扫描任务 void key_task(void *pdata) { u8 key; while(1) 538 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { key=KEY_Scan(0); if(key)OSMboxPost(msg_key,(void*)key);//发送消息 delay_ms(10); } } 该部分代码我们创建了 6 个任务:start_task、led0_task、touch_task、led1_task、main_task 和 key_task,优先级分别是 10 和 7~3,堆栈大小除了 main_task 是 128,其他都是 64。 该程序的运行流程就比上一章复杂了一些,我们创建了消息邮箱 msg_key,用于按键任务 和主任务之间的数据传输(传递键值),另外创建了信号量 sem_led1,用于 LED1 任务和主任 务之间的通信。 本代码中,我们使用了 UCOSII 提供的 CPU 统计任务,通过 OSStatInit 初始化 CPU 统计任 务,然后在主任务中显示 CPU 使用率。 另外,在主任务中,我们用到了任务的挂起和恢复函数,在执行触摸屏校准的时候,我们 必须先将触摸屏任务挂起,待校准完成之后,再恢复触摸屏任务。这是因为触摸屏校准和触摸 屏任务都用到了触摸屏和 TFTLCD,而这两个东西是不支持多个任务占用的,所以必须采用独 占的方式使用,否则可能导致数据错乱。 软件设计部分就为大家介绍到这里。 41.4 下载验证 在代码编译成功之后,我们通过下载代码到 MiniSTM32 开发板上,可以看到 LCD 显示界 面如图 41.4.1 所示: 图 41.4.1 初始界面 从图中可以看出,默认状态下,CPU 使用率仅为 1%。此时通过在触摸区域(Touch Area) 画图,可以看到 CPU 使用率飙升(42%),说明触摸屏任务是一个很占 CPU 的任务;通过按 KEY0, 539 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 可以控制 DS1 的亮灭,同时,可以在 LCD 上面看到信号量的当前值;通过按 KEY1 可以清屏; 通过按 WK_UP 可以进入校准程序,进行触摸屏校准。 540 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第四十二章 UCOSII 实验 3-消息队列、信号量集和软件定时 器 上一章,我们学习了 UCOSII 的信号量和邮箱的使用,本章,我们将学习消息队列、信号 量集和软件定时器的使用。本章分为如下几个部分: 42.1 UCOSII 消息队列、信号量集和软件定时器简介 42.2 硬件设计 42.3 软件设计 42.4 下载验证 541 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 42.1 UCOSII 消息队列、信号量集和软件定时器简介 上一章,我们介绍了信号量和邮箱的使用,本章我们介绍比较复杂消息队列、信号量集以 及软件定时器的使用。 消息队列 使用消息队列可以在任务之间传递多条消息。消息队列由三个部分组成:事件控制块、消 息队列和消息。当把事件控制块成员 OSEventType 的值置为 OS_EVENT_TYPE_Q 时,该事件 控制块描述的就是一个消息队列。 消息队列的数据结构如图 42.1.1 所示。从图中可以看到,消息队列相当于一个共用一个任 务等待列表的消息邮箱数组,事件控制块成员 OSEventPtr 指向了一个叫做队列控制块(OS_Q) 的结构,该结构管理了一个数组 MsgTbl[],该数组中的元素都是一些指向消息的指针。 图 42.1.1 消息队列的数据结构 队列控制块(OS_Q)的结构定义如下: typedef struct os_q { struct os_q *OSQPtr; void **OSQStart; void **OSQEnd; void **OSQIn; void **OSQOut; INT16U OSQSize; INT16U OSQEntries; } OS_Q; 542 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 该结构体中各参数的含义如表 42.1.1 所示: 参数 说明 OSQPtr 指向下一个空的队列控制块 OSQSize 数组的长度 OSQEntres 已存放消息指针的元素数目 OSQStart 指向消息指针数组的起始地址 指向消息指针数组结束单元的下一个单元。它使得数组构 OSQEnd 成了一个循环的缓冲区 指向插入一条消息的位置。当它移动到与 OSQEnd 相等时, OSQIn 被调整到指向数组的起始单元 指向被取出消息的位置。当它移动到与 OSQEnd 相等时,被 OSQOut 调整到指向数组的起始单元 表 42.1.1 队列控制块各参数含义 其中,可以移动的指针为 OSQIn 和 OSQOut,而指针 OSQStart 和 OSQEnd 只是一个标志 (常指针)。当可移动的指针 OSQIn 或 OSQOut 移动到数组末尾,也就是与 OSQEnd 相等时, 可移动的指针将会被调整到数组的起始位置 OSQStart。也就是说,从效果上来看,指针 OSQEnd 与 OSQStart 等值。于是,这个由消息指针构成的数组就头尾衔接起来形成了一个如图 42.1.2 所示的循环的队列。 图 42.1.2 消息指针数组构成的环形数据缓冲区 在 UCOSII 初始化时,系统将按文件 os_cfg.h 中的配置常数 OS_MAX_QS 定义 OS_MAX_QS 个队列控制块,并用队列控制块中的指针 OSQPtr 将所有队列控制块链接为链表。由于这时还 没有使用它们,故这个链表叫做空队列控制块链表。 接下来我们看看在 UCOSII 中,与消息队列相关的几个函数(未全部列出,下同)。 1) 创建消息队列函数 创建一个消息队列首先需要定义一指针数组,然后把各个消息数据缓冲区的首地址存 入这个数组中,然后再调用函数 OSQCreate 来创建消息队列。创建消息队列函数 OSQCreate 的原型为: OS_EVENT *OSQCreate(void**start,INT16U size); 其中,start 为存放消息缓冲区指针数组的地址,size 为该数组大小。该函数的返回值 为消息队列指针。 2) 请求消息队列函数 543 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 请求消息队列的目的是为了从消息队列中获取消息。任务请求消息队列需要调用函数 OSQPend,该函数原型为: void*OSQPend(OS_EVENT*pevent,INT16U timeout,INT8U *err); 其中,pevent 为所请求的消息队列的指针,timeout 为任务等待时限,err 为错误信息。 3) 向消息队列发送消息函数 任务可以通过调用函数 OSQPost 或 OSQPostFront 两个函数来向消息队列发送消息。 函数 OSQPost 以 FIFO(先进先出)的方式组织消息队列,函数 OSQPostFront 以 LIFO(后 进先出)的方式组织消息队列。这两个函数的原型分别为: INT8U OSQPost(OS_EVENT *pevent,void *msg); INT8U OSQPost(OS_EVENT *pevent,void*msg); 其中,pevent 为消息队列的指针,msg 为待发消息的指针。 消息队列还有其他一些函数,这里我们就不介绍了,感兴趣的朋友可以参考《嵌入式实时 操作系统 UCOSII 原理及应用》第五章,关于队列更详细的介绍,也请参考该书。 信号量集 在实际应用中,任务常常需要与多个事件同步,即要根据多个信号量组合作用的结果来决 定任务的运行方式。UCOSII 为了实现多个信号量组合的功能定义了一种特殊的数据结构—— 信号量集。 信号量集所能管理的信号量都是一些二值信号,所有信号量集实质上是一种可以对多个输 入的逻辑信号进行基本逻辑运算的组合逻辑,其示意图如图 42.1.3 所示 发送信号 的任务 AND/OR 请求信号 的任务 ... ... 图 42.1.3 信号量集示意图 不同于信号量、消息邮箱、消息队列等事件,UCOSII 不使用事件控制块来描述信号量集, 而使用了一个叫做标志组的结构 OS_FLAG_GRP 来描述。OS_FLAG_GRP 结构如下: typedef struct { INT8U OSFlagType; //识别是否为信号量集的标志 void *OSFlagWaitList; //指向等待任务链表的指针 OS_FLAGS OSFlagFlags; //所有信号列表 }OS_FLAG_GRP; 成员 OSFlagWaitList 是一个指针,当一个信号量集被创建后,这个指针指向了这个信号量 集的等待任务链表。 与其他前面介绍过的事件不同,信号量集用一个双向链表来组织等待任务,每一个等待任 务都是该链表中的一个节点(Node)。标志组 OS_FLAG_GRP 的成员 OSFlagWaitList 就指向了 信号量集的这个等待任务链表。等待任务链表节点 OS_FLAG_NODE 的结构如下: typedef struct { 544 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 void *OSFlagNodeNext; //指向下一个节点的指针 void *OSFlagNodePrev; //指向前一个节点的指针 void *OSFlagNodeTCB; //指向对应任务控制块的指针 void *OSFlagNodeFlagGrp; //反向指向信号量集的指针 OS_FLAGS OSFlagNodeFlags; //信号过滤器 INT8U OSFlagNodeWaitType; //定义逻辑运算关系的数据 } OS_FLAG_NODE; 其中 OSFlagNodeWaitType 是定义逻辑运算关系的一个常数(根据需要设置),其可选值和 对应的逻辑关系如表 42.1.2 所示: 常数 信号有效状态 等待任务的就绪条件 WAIT_CLR_ALL 或 WAIT_CLR_AND 0 信号全部有效(全 0) WAIT_CLR_ANY 或 WAIT_CLR_OR 0 信号有一个或一个以上有效(有 0) WAIT_SET_ALL 或 WAIT_SET_AND 1 信号全部有效(全 1) WAIT_SET_ANY 或 WAIT_SET_OR 1 信号有一个或一个以上有效(有 1) 表 42.1.2 OSFlagNodeWaitType 可选值及其意义 OSFlagFlags、OSFlagNodeFlags、OSFlagNodeWaitType 三者的关系如图 42.1.4 所示: 图 42.1.4 标志组与等待任务共同完成信号量集的逻辑运算及控制 图中为了方便说明,我们将 OSFlagFlags 定义为 8 位,但是 UCOSII 支持 8 位/16 位/32 位 定义,这个通过修改 OS_FLAGS 的类型来确定(UCOSII 默认设置 OS_FLAGS 为 16 位)。 上图清楚的表达了信号量集各成员的关系:OSFlagFlags 为信号量表,通过发送信号量集的 任务设置;OSFlagNodeFlags 为信号滤波器,由请求信号量集的任务设置,用于选择性的挑选 OSFlagFlags 中的部分(或全部)位作为有效信号;OSFlagNodeWaitType 定义有效信号的逻辑 运算关系,也是由请求信号量集的任务设置,用于选择有效信号的组合方式(0/1? 与/或?)。 举个简单的例子,假设请求信号量集的任务设置 OSFlagNodeFlags 的值为 0X0F,设置 OSFlagNodeWaitType 的值为 WAIT_SET_ANY,那么只要 OSFlagFlags 的低四位的任何一位为 545 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 1,请求信号量集的任务将得到有效的请求,从而执行相关操作,如果低四位都为 0,那么请求 信号量集的任务将得到无效的请求。 接下来我们看看在 UCOSII 中,与信号量集相关的几个函数。 1) 创建信号量集函数 任务可以通过调用函数 OSFlagCreate 来创建一个信号量集。函数 OSFlagCreate 的原 型为: OS_FLAG_GRP *OSFlagCreate (OS_FLAGS flags,INT8U *err ); 其中,flags 为信号量的初始值(即 OSFlagFlags 的值),err 为错误信息,返回值为该 信号量集的标志组的指针,应用程序根据这个指针对信号量集进行相应的操作。 2) 请求信号量集函数 任务可以通过调用函数 OSFlagPend 请求一个信号量集,函数 OSFlagPend 的原型为: OS_FLAGS OSFlagPend(OS_FLAG_GRP*pgrp, OS_FLAGS flags, INT8U wait_type, INT16U timeout, INT8U *err); 其中,pgrp 为所请求的信号量集指针,flags 为滤波器(即 OSFlagNodeFlags 的值), wait_type 为逻辑运算类型(即 OSFlagNodeWaitType 的值),timeout 为等待时限,err 为错 误信息。 3) 向信号量集发送信号函数 任务可以通过调用函数 OSFlagPost 向信号量集发信号,函数 OSFlagPost 的原型为: OS_FLAGS OSFlagPost (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U opt, INT8U *err); 其中,pgrp 为所请求的信号量集指针,flags 为选择所要发送的信号,opt 为信号有效选项, err 为错误信息。 所谓任务向信号量集发信号,就是对信号量集标志组中的信号进行置“1”(置位)或 置“0”(复位)的操作。至于对信号量集中的哪些信号进行操作,用函数中的参数 flags 来指定;对指定的信号是置“1”还是置“0”,用函数中的参数 opt 来指定(opt = OS_FLAG_SET 为置“1”操作;opt = OS_FLAG_CLR 为置“0”操作)。 信号量集就介绍到这,更详细的介绍,请参考《嵌入式实时操作系统 UCOSII 原理及应用》 第六章。 软件定时器 UCOSII 从 V2.83 版本以后,加入了软件定时器,这使得 UCOSII 的功能更加完善,在其上 的应用程序开发与移植也更加方便。在实时操作系统中一个好的软件定时器实现要求有较高的 精度、较小的处理器开销,且占用较少的存储器资源。 通过前面的学习,我们知道 UCOSII 通过 OSTimTick 函数对时钟节拍进行加 1 操作,同时 遍历任务控制块,以判断任务延时是否到时。软件定时器同样由 OSTimTick 提供时钟,但是软 件定时器的时钟还受 OS_TMR_CFG_TICKS_PER_SEC 设置的控制,也就是在 UCOSII 的时钟 节拍上面再做了一次“分频”,软件定时器的最快时钟节拍就等于 UCOSII 的系统时钟节拍。这 也决定了软件定时器的精度。 软件定时器定义了一个单独的计数器 OSTmrTime,用于软件定时器的计时,UCOSII 并不 在 OSTimTick 中进行软件定时器的到时判断与处理,而是创建了一个高于应用程序中所有其他 任务优先级的定时器管理任务 OSTmr_Task,在这个任务中进行定时器的到时判断和处理。时 钟节拍函数通过信号量给这个高优先级任务发信号。这种方法缩短了中断服务程序的执行时间, 但也使得定时器到时处理函数的响应受到中断退出时恢复现场和任务切换的影响。软件定时器 功能实现代码存放在 tmr.c 文件中,移植时需只需在 os_cfg.h 文件中使能定时器和设定定时 器的相关参数。 546 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 UCOSII 中软件定时器的实现方法是,将定时器按定时时间分组,使得每次时钟节拍到来 时只对部分定时器进行比较操作,缩短了每次处理的时间。但这就需要动态地维护一个定时器 组。定时器组的维护只是在每次定时器到时时才发生,而且定时器从组中移除和再插入操作不 需要排序。这是一种比较高效的算法,减少了维护所需的操作时间。 UCOSII 软件定时器实现了 3 类链表的维护: OS_EXT OS_TMR OSTmrTbl[OS_TMR_CFG_MAX]; //定时器控制块数组 OS_EXT OS_TMR *OSTmrFreeList; //空闲定时器控制块链表指针 OS_EXT OS_TMR_WHEEL OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE];//定时器轮 其中 OS_TMR 为定时器控制块,定时器控制块是软件定时器管理的基本单元,包含软件定 时器的名称、定时时间、在链表中的位置、使用状态、使用方式,以及到时回调函数及其参数 等基本信息。 OSTmrTbl[OS_TMR_CFG_MAX];:以数组的形式静态分配定时器控制块所需的 RAM 空间, 并存储所有已建立的定时器控制块,OS_TMR_CFG_MAX 为最大软件定时器的个数。 OSTmrFreeLiSt:为空闲定时器控制块链表头指针。空闲态的定时器控制块(OS_TMR)中, OSTmrnext 和 OSTmrPrev 两个指针分别指向空闲控制块的前一个和后一个,组织了空闲控制块 双向链表。建立定时器时,从这个链表中搜索空闲定时器控制块。 OSTmrWheelTbl[OS_TMR_CFG_WHEEL_SIZE]:该数组的每个元素都是已开启定时器的 一个分组,元素中记录了指向该分组中第一个定时器控制块的指针,以及定时器控制块的个数。 运行态的定时器控制块(OS_TMR)中,OSTmrnext 和 OSTmrPrev 两个指针同样也组织了所在分 组中定时器控制块的双向链表。软件定时器管理所需的数据结构示意图如图 42.1.5 所示: 图 42.1.5 软件定时器管理所需的数据结构示意图 OS_TMR_CFG_WHEEL_SIZE 定义了 OSTmrWheelTbl 的大小,同时这个值也是定时器分 组的依据。按照定时器到时值与 OS_TMR_CFG_WHEEL_SIZE 相除的余数进行分组:不同余数 的定时器放在不同分组中;相同余数的定时器处在同一组中,由双向链表连接。这样,余数值 为 0 ~ OS_TMR_CFG_WHEEL_SIZE-1 的 不 同 定 时 器 控 制 块 , 正 好 分 别 对 应 了 数 组 元 素 547 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 OSTmr-WheelTbl[0]~OSTmrWheelTbl[OS_TMR_CFGWHEEL_SIZE-1]的不同分组。每次时钟节 拍到来时,时钟数 OSTmrTime 值加 1,然后也进行求余操作,只有余数相同的那组定时器才有 可能到时,所以只对该组定时器进行判断。这种方法比循环判断所有定时器更高效。随着时钟 数的累加,处理的分组也由 0~OS_TMR_CFG_WHE EL_SIZE-1 循环。这里,我们推荐 OS_TMR_CFG_WHEEL_SIZE 的取值为 2 的 N 次方,以便采用移位操作计算余数,缩短处理时 间。 信号量唤醒定时器管理任务,计算出当前所要处理的分组后,程序遍历该分组中的所有控 制块,将当前 OSTmrTime 值与定时器控制块中的到时值(OSTmrMatch)相比较。若相等(即到 时),则调用该定时器到时回调函数;若不相等,则判断该组中下一个定时器控制块。如此操作, 直到该分组链表的结尾。软件定时器管理任务的流程如图 42.1.6 所示。 图 42.1.6 软件定时器管理任务流程 当运行完软件定时器的到时处理函数之后,需要进行该定时器控制块在链表中的移除和再 插入操作。插入前需要重新计算定时器下次到时时所处的分组。计算公式如下: 定时器下次到时的 OSTmrTime 值(OSTmrMatch)=定时器定时值+当前 OSTmrTime 值 新分组=定时器下次到时的 OSTmrTime 值(OSTmrMatch)%OS_TMR_CFG_WHEEL_SIZE 接下来我们看看在 UCOSII 中,与软件定时器相关的几个函数。 1) 创建软件定时器函数 创建软件定时器通过函数 OSTmrCreate 实现,该函数原型为: OS_TMR *OSTmrCreate (INT32U dly, INT32U period, INT8U opt, OS_TMR_CALLBACK callback,void *callback_arg, INT8U *pname, INT8U *perr); dly,用于初始化定时时间,对单次定时(ONE-SHOT 模式)的软件定时器来说,这 就是该定时器的定时时间,而对于周期定时(PERIODIC 模式)的软件定时器来说,这是 该定时器第一次定时的时间,从第二次开始定时时间变为 period。 period,在周期定时(PERIODIC 模式),该值为软件定时器的周期溢出时间。 opt,用于设置软件定时器工作模式。可以设置的值为:OS_TMR_OPT_ONE_SHOT 或 OS_TMR_OPT_PERIODIC,如果设置为前者,说明是一个单次定时器;设置为后者则 表示是周期定时器。 callback,为软件定时器的回调函数,当软件定时器的定时时间到达时,会调用该函数。 callback_arg,回调函数的参数。 548 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 pname,为软件定时器的名字。 perr,为错误信息。 软件定时器的回调函数有固定的格式,我们必须按照这个格式编写,软件定时器的回 调函数格式为: void (*OS_TMR_CALLBACK)(void *ptmr, void *parg); 其中,函数名我们可以自己随意设置,而 ptmr 这个参数,软件定时器用来传递当前定 时器的控制块指针,所以我们一般设置其类型为 OS_TMR*类型,第二个参数(parg)为回 调函数的参数,这个就可以根据自己需要设置了,你也可以不用,但是必须有这个参数。 2) 开启软件定时器函数 任务可以通过调用函数 OSTmrStart 开启某个软件定时器,该函数的原型为: BOOLEAN OSTmrStart (OS_TMR *ptmr, INT8U *perr); 其中 ptmr 为要开启的软件定时器指针,perr 为错误信息。 3) 停止软件定时器函数 任务可以通过调用函数 OSTmrStop 停止某个软件定时器,该函数的原型为: BOOLEAN OSTmrStop (OS_TMR *ptmr,INT8U opt,void *callback_arg,INT8U *perr); 其中 ptmr 为要停止的软件定时器指针。 opt 为停止选项,可以设置的值及其对应的意义为: OS_TMR_OPT_NONE,直接停止,不做任何其他处理 OS_TMR_OPT_CALLBACK,停止,用初始化的参数执行一次回调函数 OS_TMR_OPT_CALLBACK_ARG,停止,用新的参数执行一次回调函数 callback_arg,新的回调函数参数。 perr,错误信息。 软件定时器我们就介绍到这。 42.2 硬件设计 本节实验功能简介:本章我们在 UCOSII 里面创建 7 个任务:开始任务、LED 任务、触摸 屏任务、队列消息显示任务、信号量集任务、按键扫描任务和主任务,开始任务用于创建邮箱、 消息队列、信号量集以及其他任务,之后挂起;触摸屏任务用于在屏幕上画图,测试 CPU 使 用率;队列消息显示任务请求消息队列,在得到消息后显示收到的消息数据;信号量集任务用 于测试信号量集,采用 OS_FLAG_WAIT_SET_ANY 的方法,任何按键按下,该任务都会控制 DS1 闪一下;按键扫描任务用于按键扫描,优先级最高,将得到的键值通过消息邮箱发送出去; 主任务创建 3 个软件定时器(定时器 1,100ms 溢出一次,显示 CPU 和内存使用率;定时 2, 200ms 溢出一次,在固定区域不停的显示不同颜色;定时 3,,100ms 溢出一次,用于自动发 送消息到消息队列);KEY0 控制软件定时器 3 的开关,从而控制消息队列的发送;KEY1 控 制软件定时器 2 的开关,同时清除 LCD 触摸屏区域数据;WK_UP 按键用于触摸屏校准。 所要用到的硬件资源如下: 1) 指示灯 DS0 、DS1 2) 三个按键(KEY0/KEY1/WK_UP) 3) TFTLCD 模块 这些,我们在前面的学习中都已经介绍过了。 42.3 软件设计 本章,我们在上一章的基础上修改,由于本章要用到动态内存管理,所以拷贝实验 27 的内 549 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 存管理部分代码:MALLOC 文件夹到本例程目录下,在工程新建 MALLOC 组,并添加 malloc.c 到该组下面,然后将 MALLOC 文件夹加入头文件包含路径。 另外由于我们创建了 7 个任务,加上统计任务、空闲任务和软件定时器任务,总共 10 个任 务,如果你还想添加其他任务,请把 OS_MAX_TASKS 的值适当改大。 另外,我们还需要在 os_cfg.h 里面修改软件定时器管理部分的宏定义,修改如下: #define OS_TMR_EN 1u //使能软件定时器功能 #define OS_TMR_CFG_MAX 16u //最大软件定时器个数 #define OS_TMR_CFG_NAME_EN 1u //使能软件定时器命名 #define OS_TMR_CFG_WHEEL_SIZE 8u //软件定时器轮大小 #define OS_TMR_CFG_TICKS_PER_SEC 100u //软件定时器的时钟节拍(10ms) #define OS_TASK_TMR_PRIO 0u //软件定时器的优先级,设置为最高 这样我们就使能 UCOSII 的软件定时器功能了,并且设置最大软件定时器个数为 16,定时 器轮大小为 8,软件定时器时钟节拍为 10ms(即定时器的最少溢出时间为 10ms)。 最后,我们看看 main.c 的内容: /////////////////////////UCOSII 任务设置/////////////////////////////////// //START 任务 //设置任务优先级 #define START_TASK_PRIO 10 //开始任务的优先级设置为最低 #define START_STK_SIZE 64 //设置任务堆栈大小 OS_STK START_TASK_STK[START_STK_SIZE]; //任务堆栈 void start_task(void *pdata); //任务函数 //LED 任务 #define LED_TASK_PRIO 7 //设置任务优先级 #define LED_STK_SIZE 64 //设置任务堆栈大小 OS_STK LED_TASK_STK[LED_STK_SIZE]; //任务堆栈 void led_task(void *pdata); //任务函数 //触摸屏任务 #define TOUCH_TASK_PRIO 6 //设置任务优先级 #define TOUCH_STK_SIZE 64 //设置任务堆栈大小 OS_STK TOUCH_TASK_STK[TOUCH_STK_SIZE];//任务堆栈 void touch_task(void *pdata); //任务函数 //队列消息显示任务 #define QMSGSHOW_TASK_PRIO 5 //设置任务优先级 #define QMSGSHOW_STK_SIZE 64 //设置任务堆栈大小 OS_STK QMSGSHOW_TASK_STK[QMSGSHOW_STK_SIZE]; //任务堆栈 void qmsgshow_task(void *pdata); //任务函数 //主任务 #define MAIN_TASK_PRIO 4 //设置任务优先级 #define MAIN_STK_SIZE 128 //设置任务堆栈大小 OS_STK MAIN_TASK_STK[MAIN_STK_SIZE]; //任务堆栈 void main_task(void *pdata); //任务函数 //信号量集任务 #define FLAGS_TASK_PRIO 3 //设置任务优先级 550 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 #define FLAGS_STK_SIZE 64 //设置任务堆栈大小 OS_STK FLAGS_TASK_STK[FLAGS_STK_SIZE]; //任务堆栈 void flags_task(void *pdata); //任务函数 //按键扫描任务 #define KEY_TASK_PRIO 2 //设置任务优先级 #define KEY_STK_SIZE 64 //设置任务堆栈大小 OS_STK KEY_TASK_STK[KEY_STK_SIZE]; //任务堆栈 void key_task(void *pdata); //任务函数 OS_EVENT * msg_key; //按键邮箱事件块 OS_EVENT * q_msg; //消息队列 OS_TMR * tmr1; //软件定时器 1 OS_TMR * tmr2; //软件定时器 2 OS_TMR * tmr3; //软件定时器 3 OS_FLAG_GRP * flags_key;//按键信号量集 void * MsgGrp[256]; //消息队列存储地址,最大支持 256 个消息 //软件定时器 1 的回调函数 //每 100ms 执行一次,用于显示 CPU 使用率和内存使用率 void tmr1_callback(OS_TMR *ptmr,void *p_arg) { static u16 cpuusage=0; static u8 tcnt=0; POINT_COLOR=BLUE; if(tcnt==5) { LCD_ShowxNum(182,10,cpuusage/5,3,16,0); //显示 CPU 使用率 cpuusage=0; tcnt=0; } cpuusage+=OSCPUUsage; tcnt++; LCD_ShowxNum(182,30,mem_perused(),3,16,0); //显示内存使用率 LCD_ShowxNum(182,50,((OS_Q*)(q_msg->OSEventPtr))->OSQEntries,3,16,0X80); } //软件定时器 2 的回调函数 void tmr2_callback(OS_TMR *ptmr,void *p_arg) { static u8 sta=0; switch(sta) { case 0:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,RED); break; case 1:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,GREEN);break; case 2:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,BLUE);break; case 3:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,MAGENTA);break; case 4:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,GBLUE);break; 551 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 case 5:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,YELLOW);break; case 6:LCD_Fill(121,221,lcddev.width-1,lcddev.height-1,BRRED);break; } sta++; if(sta>6)sta=0; } //软件定时器 3 的回调函数 void tmr3_callback(OS_TMR *ptmr,void *p_arg) { u8* p; u8 err; static u8 msg_cnt=0; //msg 编号 p=mymalloc(13); //申请 13 个字节的内存 if(p) { sprintf((char*)p,"ALIENTEK %03d",msg_cnt); msg_cnt++; err=OSQPost(q_msg,p); //发送队列 if(err!=OS_ERR_NONE) //发送失败 { myfree(p); //释放内存 OSTmrStop(tmr3,OS_TMR_OPT_NONE,0,&err); } //关闭软件定时器 3 } } //加载主界面 void ucos_load_main_ui(void) { ……//省略代码 } int main(void) { delay_init(); //延时函数初始化 uart_init(72); NVIC_Configuration(); LED_Init(); //初始化与 LED 连接的硬件接口 LCD_Init(); //初始化 LCD KEY_Init(); //按键初始化 mem_init(); //初始化内存池 tp_dev.init(); ucos_load_main_ui(); OSInit(); //初始化 UCOSII OSTaskCreate(start_task,(void *)0,(OS_STK *)&START_TASK_STK [START_STK_SIZE-1],START_TASK_PRIO );//创建起始任务 552 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 OSStart(); } //开始任务 void start_task(void *pdata) { OS_CPU_SR cpu_sr=0; u8 err; pdata = pdata; msg_key=OSMboxCreate((void*)0); //创建消息邮箱 q_msg=OSQCreate(&MsgGrp[0],256); //创建消息队列 flags_key=OSFlagCreate(0,&err); //创建信号量集 OSStatInit(); OS_ENTER_CRITICAL(); //初始化统计任务.这里会延时 1 秒钟左右 //进入临界区(无法被中断打断) OSTaskCreate(led_task,(void *)0,(OS_STK*)&LED_TASK_STK[LED_STK_SIZE-1], LED_TASK_PRIO); OSTaskCreate(touch_task,(void *)0,(OS_STK*)&TOUCH_TASK_STK [TOUCH_STK_SIZE-1],TOUCH_TASK_PRIO); OSTaskCreate(qmsgshow_task,(void *)0,(OS_STK*)&QMSGSHOW_TASK_STK [QMSGSHOW_STK_SIZE-1],QMSGSHOW_TASK_PRIO); OSTaskCreate(main_task,(void *)0,(OS_STK*)&MAIN_TASK_STK [MAIN_STK_SIZE-1],MAIN_TASK_PRIO); OSTaskCreate(flags_task,(void *)0,(OS_STK*)&FLAGS_TASK_STK [FLAGS_STK_SIZE-1],FLAGS_TASK_PRIO); OSTaskCreate(key_task,(void *)0,(OS_STK*)&KEY_TASK_STK [KEY_STK_SIZE-1],KEY_TASK_PRIO); OSTaskSuspend(START_TASK_PRIO); //挂起起始任务. OS_EXIT_CRITICAL(); //退出临界区(可以被中断打断) } //LED 任务 void led_task(void *pdata) { u8 t; while(1) { t++; delay_ms(10); if(t==8)LED0=1; //LED0 灭 if(t==100) { t=0; LED0=0; } //LED0 亮 } } //触摸屏任务 void touch_task(void *pdata) { while(1) 553 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { tp_dev.scan(0); if(tp_dev.sta&TP_PRES_DOWN) { //触摸屏被按下 if(tp_dev.x[0]<120&&tp_dev.y[0]220) { TP_Draw_Big_Point(tp_dev.x[0],tp_dev.y[0],BLUE); //画图 delay_ms(2); } }else delay_ms(10); //没有按键按下的时候 } } //队列消息显示任务 void qmsgshow_task(void *pdata) { u8 *p; u8 err; while(1) { p=OSQPend(q_msg,0,&err);//请求消息队列 LCD_ShowString(5,170,240,16,16,p);//显示消息 myfree(p); delay_ms(500); } } //主任务 void main_task(void *pdata) { u32 key=0; u8 err; u8 tmr2sta=1; //软件定时器 2 开关状态 u8 tmr3sta=0; //软件定时器 3 开关状态 u8 flagsclrt=0; //信号量集显示清零倒计时 tmr1=OSTmrCreate(10,10,OS_TMR_OPT_PERIODIC,(OS_TMR_CALLBACK) tmr1_callback,0,"tmr1",&err); //100ms 执行一次 tmr2=OSTmrCreate(10,20,OS_TMR_OPT_PERIODIC,(OS_TMR_CALLBACK) tmr2_callback,0,"tmr2",&err); //200ms 执行一次 tmr3=OSTmrCreate(10,10,OS_TMR_OPT_PERIODIC,(OS_TMR_CALLBACK) tmr3_callback,0,"tmr3",&err); //100ms 执行一次 OSTmrStart(tmr1,&err);//启动软件定时器 1 OSTmrStart(tmr2,&err);//启动软件定时器 2 while(1) { key=(u32)OSMboxPend(msg_key,10,&err); if(key) { 554 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 flagsclrt=51;//500ms 后清除 OSFlagPost(flags_key,1<<(key-1),OS_FLAG_SET,&err);//设置对应信号量为 1 } if(flagsclrt)//倒计时 { flagsclrt--; if(flagsclrt==1)LCD_Fill(140,162,239,162+16,WHITE);//清除显示 } switch(key) { case KEY0_PRES://软件定时器 3 开关 tmr3sta=!tmr3sta; if(tmr3sta)OSTmrStart(tmr3,&err); else OSTmrStop(tmr3,OS_TMR_OPT_NONE,0,&err);//关闭软件定时器 3 break; case KEY1_PRES://软件定时器 2 开关&触摸区域清空 tmr2sta=!tmr2sta; if(tmr2sta)OSTmrStart(tmr2,&err); else //开启软件定时器 2 { OSTmrStop(tmr2,OS_TMR_OPT_NONE,0,&err);//关闭软件定时器 2 LCD_ShowString(148,262,240,16,16,"TMR2 STOP");//提示关闭了 } LCD_Fill(0,221,120-1,lcddev.height-1,WHITE);//触摸区域清空 break; case WKUP_PRES://校准 OSTaskSuspend(TOUCH_TASK_PRIO); //挂起触摸屏任务 OSTaskSuspend(QMSGSHOW_TASK_PRIO);//挂起队列信息显示任务 OSTmrStop(tmr1,OS_TMR_OPT_NONE,0,&err);//关闭软件定时器 1 if(tmr2sta)OSTmrStop(tmr2,OS_TMR_OPT_NONE,0,&err);//关闭 tmr2 if((tp_dev.touchtype&0X80)==0)TP_Adjust(); OSTmrStart(tmr1,&err); //重新开启软件定时器 1 if(tmr2sta)OSTmrStart(tmr2,&err); //重新开启软件定时器 2 OSTaskResume(TOUCH_TASK_PRIO); //解挂 OSTaskResume(QMSGSHOW_TASK_PRIO); //解挂 ucos_load_main_ui(); //重新加载主界面 break; } delay_ms(10); } } //信号量集处理任务 void flags_task(void *pdata) 555 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 { u16 flags; u8 err; while(1) { flags=OSFlagPend(flags_key,0X001F,OS_FLAG_WAIT_SET_ANY,0,&err);//等待 if(flags&0X0001)LCD_ShowString(140,162,240,16,16,"KEY0 DOWN "); if(flags&0X0002)LCD_ShowString(140,162,240,16,16,"KEY1 DOWN "); if(flags&0X0004)LCD_ShowString(140,162,240,16,16,"KEY_UP DOWN"); LED1=0; delay_ms(50); LED1=1; OSFlagPost(flags_key,0X0007,OS_FLAG_CLR,&err);//全部信号量清零 } } //按键扫描任务 void key_task(void *pdata) { u8 key; while(1) { key=KEY_Scan(0); if(key)OSMboxPost(msg_key,(void*)key);//发送消息 delay_ms(10); } } 本章 test.c 的代码有点多,因为我们创建了 7 个任务,3 个软件定时器及其回调函数,所以, 整个代码有点多,我们创建的 7 个任务为:start_task、led_task、touch_task、qmsgshow_task 、 main_task、flags_task 和 key_task,优先级分别是 10 和 7~2,堆栈大小除了 main_task 是 128, 其他都是 64。 我们还创建了 3 个软件定时器 tmr1、tmr2 和 tmr3,tmr1 用于显示 CPU 使用率和内存使用 率,每 100ms 执行一次;tmr2 用于在 LCD 的右下角区域不停的显示各种颜色,每 200ms 执行 一次;tmr3 用于定时向队列发送消息(用到了动态内存申请),每 100ms 发送一次。 本章,我们依旧使用消息邮箱 msg_key 在按键任务和主任务之间传递键值数据,我们创建 信号量集 flags_key,在主任务里面将按键键值通过信号量集传递给信号量集处理任务 flags_task, 实现按键信息的显示以及 DS1 的提示性闪灯。 本章,我们还创建了一个大小为 256 的消息队列 q_msg,通过软件定时器 tmr3 的回调函数 向消息队列发送消息,然后在消息队列显示任务 qmsgshow_task 里面请求消息队列,并在 LCD 上面显示得到的消息。消息队列还用到了动态内存管理。 在主任务 main_task 里面,我们实现了 42.2 节介绍的功能:KEY0 控制软件定时器 3 的开 关,间接控制消息队列的发送;KEY1 控制软件定时器 2 的开关,同时清除 LCD 触摸屏区域的 数据;WK_UP 用于触摸屏校准,在校准的时候,要先挂起触摸屏任务、队列消息显示任务, 并停止软件定时器 tmr1 和 tmr2,否则可能对校准时的 LCD 显示造成干扰; 软件设计部分就为大家介绍到这里。 556 42.4 下载验证 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 在代码编译成功之后,我们通过下载代码到 MiniSTM32 开发板上,可以看到 LCD 显示界 面如图 42.4.1 所示: 图 42.4.1 初始界面 从图中可以看出,默认状态下,CPU 使用率为 8%左右,比上一章多一些,这主要是软件 定时器 2(tmr2)不停的刷屏导致的。 通过按 KEY0,控制软件定时器 3(tmr3)的开关,从而控制消息队列的发送;可以在 LCD 上面看到 Q 和 MEM 的值慢慢变大(说明队列消息在增多,占用内存也随着消息增多而增大), 在 QUEUE MSG 区,开始显示队列消息,再按一次 KEY0 停止 tmr3,此时可以看到 Q 和 MEM 逐渐减小。当 Q 值变为 0 的时候,QUEUE MSG 也停止显示(队列为空)。 通过按 KEY1 控制软件定时器 2 的开关,同时清除 LCD 触摸屏区域数据, 通过 WK_UP 按键,可以进行触摸屏校准。 在 TOUCH 区域,可以输入手写内容。 任何按键按下,DS1 都会闪一下,提示按键被按下,同时在 FLAGS 区域显示按键信息。 557 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 第四十三章 MiniSTM32 开发板综合实验 前面已经给大家讲了 37 个实例了,本章将设计一个综合实例,作为本教程的最后一个实验 ,该实验向大家展示了 STM32 的强大处理能力,并且可以测试开发板的大部分功能。该实验 代码非常多,涉及 GUI(ALIENTEK 编写,非 ucGUI)、UCOS、内存管理、图片解码、文件系 统、USB、手写识别、汉字输入等非常多的内容,故本章不讲实现和代码,只讲功能,本章将 分为如下几个部分: 43.1 MiniSTM32 开发板综合实验简介 43.2 MiniSTM32 开发板综合实验详解 558 43.1 MiniSTM32 开发板综合实验简介 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 MiniSTM32 V3.0 开发板搭载了 STM32F103RCT6 处理器,拥有较多的 FLASH 和 SRAM, 可以实现更加强大的应用。开发板的硬件资源在第一章我们已经详细介绍过,是比较强大的, 强大的硬件必须配强大的软件才能体现其价值,如果 IPhone 装的是 andriod 而不是 ios,IPhone 就不是那个 IPhone 了,可能早就被三星打败了。同样,如果开发板只是一堆硬件,那就和一堆 废品差不多。 MiniSTM32 V3.0 开发板的综合测试实验,移植自战舰 STM32 开发板的综合实验(裁剪了 一部分),所以看起来和战舰板的综合实验界面基本一样,接下来我们就看看 MiniSTM32 开发 板综合实验的具体功能吧。 MiniSTM32 开发板综合实验总共有 9 大功能,分别是:电子图书、数码相框、USB 连接、 应用中心、时钟、系统设置、画板、无线传书和记事本。 电子图书,支持.txt/.c/.h/.lrc 等 4 种格式的文件阅读。 数码相框,支持.bmp/.jpeg/.jpb/.gif 等 4 种格式的图片文件播放。 USB 连接,支持和电脑连接读写 SD 卡/SPI FLASH 的内容。 应用中心,可以扩展 16 个应用程序,我们实现了其中 1 个,其他留给大家自己扩展。 时钟,支持温度、时间、日期、星期的显示,并支持闹钟功能。 系统设置,整个综合实验的设置。 画板,可以作画/对 bmp 图片进行编辑,支持画笔颜色/尺寸设置。 无线传书,通过无线模块,实现两个开发板之间的无线通信。 记事本,可以实现文本(.txt/.c/.h/.lrc)记录编辑等功能,支持中英文输入,手写识别。 以上,就是综合实验的 9 个功能简介,涉及到的内容包括:GU(I ALIENTEK 编写,非 ucGUI)、 UCOS、内存管理、图片解码、文件系统、USB、手写识别、汉字输入等非常多的内容。下面, 我们将详细介绍这 9 个功能。 43.2 MiniSTM32 开发板综合实验详解 要测试 MiniSTM32 开发板综合实验的全部功能,大家得自备一个 SD 卡。不过,就算没有 SD 卡,综合实验还是可以正常运行的,只是有些限制而已,比如:不能保存新建的记事本、 不能保存新建的画图等。除了这些外,其他功能都可以正常运行。 我们先来看看 MiniSTM32 开发板综合实验的启动界面,启动界面如图 43.2.1 所示: 559 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.1 综合实验启动界面 注意:综合实验支持屏幕截图(通过 USMART 控制,波特率为 115200),本章所有图片均 来自屏幕截图! 上图显示了综合实验的详细启动过程,首先显示了版权信息,软硬件版本,接着显示了 LCD 驱动器的型号(LCD ID),然后显示 CPU 和内存信息,之后显示 SPI FLASH 的大小,接着开 始初始化文件系统(FATFS),然后显示 SD 卡容量和 FLASH Disk 容量(注意 FLASH Disk 就 是指 SPI FLASH,因为我们划分了 4.8M 空间给 FATFS 管理,所以 FLASH Disk 的容量为 4896KB)。 接着,就是硬件检测,完了之后检测字库和系统文件,再初始化触摸屏,加载系统参数(参 数保存在 24C02 里面),最后启动系统。在加载过冲中,任何一个地方出错,都会显示相应的 提示信息,请在检查无误后,按复位重启。 这里有几个注意的地方: ① 如果没插入 SD 卡,会显示 SD Card ERROR,不过系统还是会继续启动,因为没有 SD 卡系统还是可以启动的(前提是 SPI FLASH(W25Q64)里面的系统文件和字库文件都 是正常的)。 ② 系统文件和字库文件都是存在 SPI FLASH(W25Q64)里面的,如这两个文件被破坏了, 在启动的时候,会执行字库和系统文件的更新,此时你得准备一个 SD 卡,并拷贝 SYSTEM 文件夹(注意:这个 SYSTEM 文件夹不是开发板例程里的 SYSTEM 文件夹, 而是光盘根目录SD 卡根目录文件SYSTEM 文件夹)到 SD 卡根目录,以便系统更 新时使用。 ③ FLASH Disk 是从 SPI FLASH(W25Q64)里面分割 4.8M 空间出来实现的,强制将 4K 字节的扇区改为 512 字节使用,所以在写操作的时候擦除次数会明显提升(8 倍以上), 因此,如非必要,请不要往 FLASH Disk 里面写文件。频繁的写操作,很容易将 FLASH Disk 写挂掉。 ④ 在系统启动时,一直按着 KEY0 不放(加载到 Touch Check 的时候),可以进入强制校 准。当你发现触摸屏不准的时候,可以使用这个办法强制校准。 ⑤ 在系统启动时,一直按着 KEY1 不放(加载到 Font Check 的时候),可以强制更新字库。 560 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 ⑥ 本系统用到触摸按键 KEY0 做返回(类似手机的 HOME 键),当发现界面没有返回按 钮的时候,可以按 KEY0 按键返回。 ⑦ 如果插入了 SD 卡,系统在启动的时候,会在 SD 卡的根目录创建 2 个文件夹:TEXT 和 PAINT。其中,TEXT 文件夹用来保存新建的文本文件(记事本功能时使用);PAINT 文件夹用来保存新建的画板文件(手写画笔功能时使用)。 在 SYSTEM Starting…之后,系统启动 UCOSII,并加载 SPB 界面,在加载成功之后,来到 主界面,主界面如图 43.2.2 所示: 图 43.2.2 综合实验系统主界面 这里主界面默认是简体中文的,我们可以在系统设置里面设置语言,MiniSTM32 开发板综 合实验支持 3 种语言选择:简体中文、繁体中文和英文。 在进入主界面之后,开发板上的 DS0 开始有规律的短亮(每 2.5 秒左右亮 100ms),提示系 统运行正常,我们可以通过 DS0 判断系统的运行状况。另外,如果运行过程中,出现 HardFault 的情况,系统则会进入 HardFault 中断服务函数,此时 DS0 和 DS1 都会闪烁,提示系统故障。 同时在串口打印故障信息。通过串口,系统会打印其他很多信息,最常打印的是内存使用率, 然后我们还可以通过 USMART 对系统进行调试。 如图 43.2.2 所示,综合实验的主界面总共 9 个功能图标,我们可以随便点击一个即可选中, 如图 43.2.3 所示: 561 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.3 选中电子图书 从上图可以看出,选中之后,图标发生了一点点变化,手机图标也是类似的效果,其实就 是一个 alphablend。再次点击该图标,我们就可以进入电子图书功能。 在任何界面下,都可以通过按 KEY0 返回上一级,直至返回到主界面。 在介绍完系统启动之后,我们开始介绍各个功能。 43.2.1 电子图书 双击主界面的电子图书图标,进入如图 43.2.1.1 所示的文件浏览界面: 图 43.2.1.1 文件浏览界面 上图中,左侧的图是我们刚刚进入的时候看到的界面(类似在 XP 上打开我的电脑),可以 看到我们有 2 个盘:正点原子(SD 卡)和 ALIENTEK(SPI FLASH 磁盘)。我们可以选择任 562 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 何一个打开,并浏览里面的内容。注意,图中的正点原子和 ALIENTEK,分别是 SD 卡和 FLASH 磁盘的卷标名字(LABEL),这个我们可以自己在电脑上修改为任何你喜欢的名字。 界面的上方显示文件/文件夹的路径。如果当前路径是磁盘/磁盘根目录则显示磁盘图标, 如果是文件夹,则显示文件夹图标,另外,如果路径太深,则只显示部分路径(其余用…代替)。 界面的下方显示磁盘/文件夹信息。 界面的下方,显示磁盘信息/当前文件夹信息。对磁盘,则显示当前选中磁盘的总容量和可 用空间,对文件夹,则显示当前路径下文件夹总数和文件总数,并显示你当前选中的是第几个 文件夹/文件。 双击打开 SD 卡,得到界面如右侧图片所示,此时,因为 SD 卡根目录的文件数目超过了 1 页所能显示的数目,所以在右侧出现了滚动条,我们可以拖动滚动条/按滚动条两端的按钮/直 接在屏幕中心区域拖动,来查找你要打开的文件/文件夹。 选中一个文件夹,双击打开得到如图 43.2.1.2 所示界面: 图 43.2.1.2 目标文件和文本阅读 上图左侧显示了当前文件夹下面的目标文件(即电子图书支持的文件,包括.txt/.h/.c/.lrc 等 格式,其中.txt/.h/.c 文件共用 1 个图标,.lrc 文件单独一个图标)。另外,如果文件名太长,在 我们选中该文件名后,系统会以走字的形式,显示整个文件名。 我们打开一个 lrc 文件,开始文本阅读,如图右侧的图片所示,同样我们可以通过滚动条/ 拖动的方式来浏览,图中我们还看到有一个光标,触摸屏点到哪,它就在哪里闪烁,可以方便 大家阅读。 文本阅读是将整个文本文件加载到外部内存里面来实现的,所以文本文件最大不能超过外 部内存总大小,即 35KB(这里仅指受内存管理的部分,不是整个 SRAM 的大小)。 当我们想退出文本阅读的时候,通过按 KEY0 按键实现,按一下 KEY0,则又回到查找目 标文件状态(左侧图),按返回按钮可以返回上一层目录,如果再按一次 KEY0 则直接返回主 界面。 563 43.2.2 数码相框 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 双击主界面的数码相框图标,进入文件浏览界面,这个和 43.2.1 节差不多,我们找到存放 图片的文件夹,如图 43.2.2.1 所示: 图 43.2.2.1 文件浏览和图片播放 左侧是文件浏览的界面,可以看到在 PICTURE 文件夹下总共有 54 个文件(当前选中是第 5 个),包括 gif/jpg/bmp 等,这些都是数码相框功能所支持的格式。右侧图片显示了一个正在播 放的 GIF 图片,并在其左上角显示当前图片的名字。当然,我们也可以播放 jpg 和 bmp 文件, 如图 43.2.2.2 所示: 图 43.2.2.2 jpg 和 bmp 图片播放 564 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 对于 jpg 和 bmp 文件,基本没有尺寸限制(但图片越大,解码时间越久),但是对于 gif 文 件,则只支持尺寸在 240*320 以内的文件(因为 gif 图片我们不好做尺寸压缩处理),超过这个 尺寸的 gif 图片将无法显示!! 我们可以通过按屏幕的上方(1/3 屏幕)区域切换到上一张图片浏览;通过按屏幕的下方 (1/3 屏幕)区域切换到下一章图片;通过单击屏幕的中间(1/3 屏幕)区域可以暂停自动播放, 同时 DS1 亮,提示正在暂停状态,双击屏幕的中间区域会弹出返回按钮,如图 43.2.2.3 所示: 图 43.2.2.3 弹出返回按钮 此时,我们可以通过按返回按钮返回文件浏览状态,当然也可以通过按 KEY0 按键,直接 返回文件浏览状态(不需要等返回按钮弹出)。 图片浏览支持两种自动播放模式:循环播放/随即播放。大家可以在系统设置里面设置图片 播放模式。系统默认是循环播放模式,在该模式下,每隔 4 秒左右自动播放下一张图片,依次 播放所有图片。而随机播放模式,也是每隔 4 秒左右自动播放下一张图片,但是不是顺序播放, 而是随机的播放下一张图片。 43.2.3 USB 连接 双击主界面的 USB 连接图标,如果开发板的 USB 端口没有连接电脑,则显示无连接,如 图 43.2.3.1 所示: 565 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.3.1 USB 无连接和 USB 读数据 上图中,左侧的图片显示开发板没有和电脑连接上,此时,我们找一根 USB 线,连接开发 板的 USB 端口(侧面的 USB 口)和电脑的 USB(注意,不要同时将 USB_232 与 USB 都插到 同一台电脑上)。此时,可以看到开发板提示 USB 已连接,并显示 USB 正在读数据,同时我们 在电脑上面,可以看到右下角提示发现新硬件(如果是第一次连接的话),如图 43.2.3.2 所示: 图 43.2.3.2 电脑发现新硬件 此时,我们打开我的电脑,即可找到 2 个可移动磁盘,分别为开发板的 SD 卡和 FLASH Disk。 这样,我们就实现了开发板和电脑的 USB 连接,可以直接从电脑拷贝文件到开发板的 SD 卡或 者 FLASH Disk(名字为:ALIENTEK,即 W25Q64)。 这里再次提醒大家,如非必要,不要往 FLASH Disk 写入数据!否则容易写坏 SPI FLASH。 43.2.4 应用中心 双击主界面的应用中心图标,进入应用中心界面,如图 43.2.4.1 所示: 566 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.4.1 应用中心和红外遥控 左侧图片是我们刚进入应用中心看到的界面,在该界面下总共有 16 个图标,我们仅实现了 第一个:红外遥控功能。其他都没有实现,大家可以自由发挥,添加属于自己的东西。双击第 一个图标,会弹出一个红外遥控的小窗口,用于接收红外信号,如图 43.2.4.1 右侧图片所示。 此时,我们将红外遥控对准 MiniSTM32 开发板的红外接收头,并按钮,则可以在红外遥控 窗体里面显示键值、按键次数、符号等信息。如图 43.2.4.2 所示: 图 43.2.4.2 红外按键解码 图中,我们按下了红外遥控器下的两个按键,分别得到两个按键的键值、次数和符号等信 息。其中次数是代表我们持续按下红外遥控某个按键的时长,越长该值越大。 567 43.2.5 时钟 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 双击主界面的时钟图标,进入时钟界面,如图 43.2.5.1 所示: 图 43.2.5.1 时钟界面 图 43.2.5.1 的左侧图片为加载时钟界面时的提示界面,表明没有检测到 18B20,启用内部 温度传感器,之后进入时钟主界面,如右侧图片所示。在时钟界面,我们显示了日期、时间、 温度、星期等信息。另外,我们可以在系统设置里面设置时间和日期,还可以设置闹钟,这个 我们后面再介绍。 图中的温度是通过 STM32 自带的温度传感器采集的,所以有点偏高,如果我们在开发板 的 U13 处插入 DS18B20,则会采集来自 18B20 的温度,这样就比较准确了,不过请特别注意: 仅在需要进入时钟界面时,才将 PA0 和 1820 的跳线帽短接,否则,请断开 PA0 和 1820 的跳 线帽,以免影响 WK_UP 按键的使用!! 如果需要返回主界面,请按 KEY0 按键!! 43.2.6 系统设置 双击主界面的系统设置图标,进入系统设置界面,如图 43.2.6.1 所示: 568 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.6.1 系统设置主界面和时间设置界面 上图左侧图片为系统设置主界面,在系统设置里面,总共有 12 个项目:时间设置、日期设 置、闹钟时间设置、闹钟开关设置、闹钟提示设置、语言设置、数码相框设置、屏幕校准、系 统文件更新、系统信息、系统状态、关于。通过这 12 个项目,我们可以设置和查看各种系统参 数。下面我们将一一介绍这些设置。 首先是时间设置,如图 43.2.6.1 右侧图片所示,双击时间设置,就会弹出时间设置对话框, 我们就可以设置开发板的时间了。设置好之后点击确定回到系统设置主界面,如果想放弃设置, 则直接点击取消(或按 KEY0)。 再来看看日期设置和闹钟时间设置,如图 43.2.6.2 所示: 569 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.6.2 日期设置和闹钟时间设置 上图中,左侧的对话框用来设置系统日期,右侧的对话框用来设置闹钟时间。操作上同前 面介绍的时间设置的方法一模一样。关于闹钟,我们等下再详细介绍,先看闹钟开关设置和闹 钟提示设置两个界面,如图 43.2.6.3 所示: 图 43.2.6.3 闹钟开关设置和闹钟提示设置 上图中,左侧对话框用来设置闹钟开关,右侧对话框用来设置闹钟提示。这里,我们来介 绍一下本系统的闹钟,本系统的闹钟以星期为周期,以时间为点实现闹钟,比如判断一个闹钟 是否应该响铃的标准是:先判断星期的条件是否满足,比如上图我们设置是周一到周五闹铃, 今天(2014 年 3 月 19 号)是周三,所以满足星期条件,接着看时间是否相等,如果两个条件 都满足,则闹铃。从前面的时间设置我们知道当前时间是 15:01 分,而上图我们设置的闹钟时 间是 15:03,所以时间还不相等,故不闹铃,当时间来到 15:03 的时候,系统将会闹铃。闹钟提 示的方式有 4 种,如上图右侧图片所示,提示通过 DS1 产生,不同的设置,DS1 的闪烁方式不 同。当闹钟时间到来的时候,产生闹铃,如图 43.2.6.4 所示: 570 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.6.4 闹铃和语言设置 上图中,左侧的图片显示正在闹铃。此时会弹出一个小的闹钟时间,提示当前的闹铃时间, 同时可以看到 DS1 闪烁(每秒闪 3 次,即闹钟提示 3)。点击屏幕上的闹铃时间(或按 KEY0) 可以关闭当前闹铃。右侧的图片为语言设置界面,系统支持 3 种语言设置,默认为简体中文, 设置为繁体中文/English 之后如图 43.2.6.5 所示: 图 43.2.6.5 繁体中文和 English 上图显示了繁体中文和 English 的设置,不过本章我们还是以简体中文为例进行介绍。下 面,我们来看看数码相框设置和屏幕校准,如图 43.2.6.6 所示: 571 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.6.6 数码相框设置和屏幕校准 前面提到数码相框支持全部循环播放和随机播放两种模式,就是通过上图左侧的界面设置 的。 右侧为触摸屏校准界面,这个校准界面和手机校准界面基本类似,校准的时候,请用触笔 (或者其他尖一点的东西)依次点击 4 个十字圈的最中心(图中是第一个,如果点中会自动弹 出第二个,共 4 个),在 4 个校准点都准确点击之后,系统提示:Touch Screen Adjust OK!。如 果校准失败,则提示失败信息,请重新校准,直到校准成功。另外,在该界面下,如果连续 10 秒没有输入的话,系统会自动退出校准界面,当然,我们也可以按 KEY0 直接退出。 接下来,我们看看系统文件更新,如图 43.2.6.7 所示: 图 43.2.6.7 系统文件更新 572 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图中,左侧的界面为系统文件更新提示界面,这里的系统文件是指 SYSTEM 文件夹里面的 所有内容。MiniSTM32 开发板综合例程之所以可以没有 SD 卡也能正常运行,主要是将 SYSTEM 文件夹(注意这个不是源码里面的 SYSTEM 文件夹!!)拷贝到了 FLASH Disk(即 W25Q64) 里面,这样,我们所有的系统资源都可以从 W25Q64 里面获得,从而正常启动。 SYSTEM 文件夹目前是包含 34 个文件,总大小为 3.41MB,包括 29 个图片/图标,另外包 括 5 个字库相关文件。这些文件一般不要修改,如果你想自己 DIY 的话,那可以修改这些文件, 以达到你要的效果,不过建议修改之前备份一下,搞坏了还可以还原。 如果在图 43.2.6.7 的系统文件更新提示时选择:确定,则会执行系统文件更新,将 SD 卡 的 SYSTEM 文件夹,拷贝到 FLASH Disk 里面。这里有个前提,就是你的 SD 卡里面必须有这 个 SYSTEM 文件夹!更新时界面如图 43.2.6.7 右侧图片所示。 接下来,我们看看系统信息和系统状态,图 43.2.6.8 所示: 图 43.2.6.8 系统信息和系统状态 上图中,左侧的界面为系统信息界面,通过该界面,可以看到软硬件的详细信息。右侧的 界面显示了当前系统资源状况,显示了当前 CPU 使用率,CPU 温度以及内存使用率。 最后,我们来看看系统状态和关于界面,如图 43.2.6.9 所示: 573 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.6.9 关于界面 上图就是关于界面,显示了 MiniSTM32 开发板的软硬件版本以及产品序列号,这个序列号 是全球唯一的,每个开发板都不一样。 43.2.7 画板 双击主界面的画板图标,首先弹出模式选择对话框,如图 43.2.7.1 所示: 图 43.2.7.1 模式选择和画笔颜色设置 上图中,左侧图片为我们双击手写画笔后,弹出的模式选择界面,我们可以选择新建画笔, 建立一个新的文件;也可以选择打开一个已有的位图进行编辑。右侧的图片为我们新建画笔后, 按 WK_UP 弹出的画笔颜色设置对话框。默认画笔尺寸为 3,颜色为红色,通过这个画笔颜色 574 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 设置对话框,我们可以选择自己需要的颜色,该对话框提供 20 种常用颜色,左侧的正方形区域 为画笔颜色预览区,通过右侧的颜色条大家可以选择自己喜欢的颜色。 画笔尺寸设置界面如图 43.2.7.2 所示: 图 43.2.7.2 画笔尺寸设置和完成后的画图 上图中,左侧为画笔尺寸设置界面,我们可以通过滚动条设置画笔尺寸,左侧会显示画笔 尺寸的预览图。右侧的图片为我们完成的画图文件,在返回主界面(按 KEY0)的时候,会提 示保存,如图 43.2.7.3 所示: 图 43.2.7.3 保存画图和编辑已有位图 上图中,左侧为我们退出时(按 KEY0)弹出的提示保存对话框,如果选择确定,新的画 575 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 文 件 将 会 被 保 存 在 SD 卡 的 PAINT 文 件 夹 里 面 , 命 名 方 式 是 以 时 间 命 名 的 , 如 PAINT20140319223950.bmp。 右侧的图片为对打开的位图进行编辑的界面,通过这个功能,我们可以在开发板上实现对 一些相片(bmp 格式)进行涂鸦。 最后,提醒大家,MiniSTM32 V3.0 的画板功能,是支持电容触摸屏(比如 ALIENTEK 4.3 寸电容触摸屏)的,可以支持多点触摸。 43.2.8 无线传书 该功能用来实现两个开发板之间的无线数据传输,在开发板 A 输入的内容,会在开发板 B 上完整的“复制”一份,该功能需要 2 个开发板(MiniSTM32 开发板[刷实验 38])和 2 个 NRF24L01 无线模块。 双击主界面的无线传书图标(假定开发板已插上 NRF24L01 无线模块),会先弹出模式选 择对话框,如图 43.2.8.1 所示: 图 43.2.8.1 模式选择和发送模式界面 从左侧的图片可以看出,模式选择,我们可以设置为发送模式或接收模式。右侧的图片则 是选择发送模式后进入的界面。我们在另外一块开发板(开发板 B)设置模式为接收模式,然 后在本开发板(开发板 A)手写输入一些内容,就可以看到在另外一个开发板也出现了同样的 内容,如图 43.2.8.2 所示: 576 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 图 43.2.8.2 在开发板 A 输入的内容完整的显示在开发板 B 上 从上图可以看出,在开发板 A 上输入的内容,被完整的复制到开发板 B 上了。这就是无线 传书功能。 43.2.9 记事本 双击主界面的记事本图标,首先弹出模式选择对话框,如图 43.2.9.1 所示: 图 43.2.9.1 模式选择和新建文本文件 记事本支持 2 种模式:1,新建文本文件,这种方式完全新建一个文本文件(以当前系统时 577 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 间命名),用来输入信息。2,打开已有文件,这种方式可以对已有的文件进行编辑。 上图中,右侧的界面为我们选择新建文本文件后的界面,此时出现一个空白编辑区和一个 闪烁的光标,我们通过下方的键盘输入信息即可,这个输入键盘和我们的手机键盘十分类似, 输入方法也是一模一样,支持中文、字母、数字和手写识别输入等几种输入方式。中文输入和 标点符号输入,如图 43.2.9.2 所示: 图 43.2.9.2 中文输入和标点符号输入 中文输入我们使用 T9 拼音输入法(但不支持联想),关于这个中文输入法的介绍,大家可 以参考战舰板的例程。该键盘还支持英文输入和手写识别输入,如图 43.2.9.3 所示: 图 43.2.9.3 英文输入和手写识别输入 578 STM32 不完全手册(库函数版) ALIENTEK MiniSTM32 V3.0 开发板教程 上图中,左侧的图片为英文输入界面,比较简单;右侧的图片为手写识别的输入界面,手 写识别采用 ALIENTEK 提供的手写识别库,具体的使用方法见战舰板手写识别实验。 只要新建文本文件有被编辑过,那么在返回(按 KEY0 返回)的时候,系统会提示是否保 存,如图 43.2.9.4 所示: 图 43.2.9.4 保存提示和编辑已有文件 上图中,左侧图片为提示保存界面,如果选择确定,该文件将被保存在 SD 卡根目录的 TEXT 文件夹里面。右侧图片为打开已有文件进行编辑的界面,这样我们就可以在 MiniSTM32 开发板 上编辑.txt/.h/.c/.lrc 文件了。 至此,整个 MiniSTM32 开发板的综合测试实验就介绍完了。很多是移植自战舰板的综合实 验,希望我们的这个代码,可以让大家有所受益,能开发出更强更好的产品。 综合实验整个代码编译后大小为 227K 左右,代码量