I/O基本结构

image.png
三种输入方式:
(1)一般数字输入
所走路径为①到④,间要经过“斯密特触发器”和“输入数据寄存器”。
a.斯密特触发器的作用
由于从引脚输入的高低电平信号不是很完美,所以需要经过斯密特触发器的修整,经过修整之后信号就会变得更稳定、更漂亮。
b.输入寄存器的作用
输入的数字信号(数据)将缓存在“输入数据寄存器”中,然后程序即可从“输入数据寄存器”中读出输入的数据,拿到数据后,程序就可以根据需要来使用这个数据了。

(2)复用功能输入:
所走路径为①到③,其输入的数字信号经过“斯密特触发器”的修整之后,就可以交给复用该引脚的其它“片内外设”的寄存器保存起来了,比如 USB、DMA、UART 等,这些“片内外设”拿到数据后,就会按照自己的要求来使用这些数据。
使用复用输入时,除了要配置 GPIO 的寄存器外,还需要配置 USB、DMA、UART 这些外设的寄存器,让这些“片内外设”工作起来后,它们才能接收“复用输入”的数据。

(3)模拟输入
所走路径为①到②,因为模拟信号不需要修正,所以不经过“斯密特触发器”。模拟信号输入进到芯片内部后,就会交给处理模拟信号的“片内外设”,比如交给 AD,AD 就会进行模拟/数字的转换,将模拟信号变为数字信号。

什么时候使用模拟输入
比如,温度传感器将温度转为模拟电信号后,没有将模拟信号变为数字信号,此时芯片为了能够处理这个模拟信号,就需要先通过模拟输入得到模拟信号,然后芯片内部的 AD 再将其转为数字信号,然后才能使用这个数字信号温度值。
不过这种情况不多见,因为现在大多数的传感器都自带 AD,会直接将模拟信号转为数字信号,最终给芯片的其实都已经是数字信号了

三种输入路径:
(1)一般的数字输出
路径为⑤①或者⑥①,这两种其实是一样的,唯一不同的是,

  • ⑤①路径:程序将数据先讲写入“复位/置位寄存器”(标记 e),然后电路再自动将数据导入“输出数据寄存器”(标记 f)中,然后在输出。
  • ⑥①路径:程序直接将数据写入“数据输出寄存器”,然后再输出。

问1:为什么在 ⑤①路径中,数据要先写入“复位/置位寄存器”?
涉及到“复位/置位寄存器”和“输出数据寄存器”之间区别
问2:为什么还可以读“输出数据寄存器”?
我们举例理解,假如我想将原来“输出数据寄存器”中数据的第 3bit 改为 0 后再输出,可是我并不知道原来里面的值是多少,此时应该怎么办呢?我们的程序就可以先读出“输出数据寄存器”中原来的数据,然后将第 3bit 改为 0,然后再重新回写到“输出数据寄存器”中。

PS:
初看这张图的很多人总有一种误区,总以为每个引脚都有完全独立的一套寄存器,其实不是这样的,真实的情况是每个端口的 16 个引脚,比如 GPIOB 端口的 16 个引脚,它们共用相同的“输入数据寄存器”、“输出数据寄存器”、“复位/置位寄存器”等寄存器,不过每个端口的寄存器肯定都是独立的,比如 GPIOA 端口有自己独立的“输入数据寄存器”,GPIOB、GPIOC 等端口也有自己独立的“输入数据寄存器”。

问3:图中的“输出控制”是个啥?(标记 g)
“输出控制”这个东西用于选择“输出类型”,数字信号的输出可以有两种“输出类型”,一种为“推挽输出”,另一种为“开漏输出”。每个 GPIOx 端口都有提供专门的寄存器,以供我们选择是使用“推挽输出”,还是使用“开漏输出”。
(2)复用输出
输出路径为⑦①,复用输出时可以是“推挽输出”的或者“开漏输出”的,看需求。
假设引脚使用的是 LCD 相关的复用,此时程序会先将图像数据交给“LCD 片内外设”,片内外设再将数据通过“复用输出路径”将输出从引脚输出,最终将数据交给 LCD 液晶以供显示。

(3)模拟输出
输出路径为⑧①,我们举一个会用到模拟输出的例子,比如芯片内部的 DA 将数字信号转为了模拟信号,此时片内模拟信号的输出,走的就是“模拟输出路径”。

端口配置表

image.png

1.硬件:

开发板:
image.png
GPIO负极接地
IO->1 亮
IO->2 灭

2.MDK工程建立

注意:单片机中用到的C语言其实不是标准的,而是有点定制性的C语言。
之前认识:整个程序从main函数开始执行,main执行完了整个程序就完了。
起始代码:从CPU复位开始执行的第1句指令,到main函数之前所做的事情就是起始代码。

3.关于起始代码

(1)起始代码是哪里来的
image.png
image.png
(2)不同的CPU的其实代码一般是不同的
(3)起始代码是用汇编写的,一般不需要看懂,知道点就行了

3.寄存器信息确认

PG6 PG7
(1)STM32 PortG的起始地址是:0x4001 2000
(2)有可能涉及到的GPIO的地址:
寄存器名 偏移量 寄存器地址
GPIOG_CRL 0x00 0x40012000
GPIOG_CRH 0x04 0x40012004
GPIOG_IDR 0x08 0x40012008 //输入模式寄存器
GPIOG_ODR 0x0C 0x4001200C
输出模式寄存器
GPIOG_BSRR 0x10 0x40012010 ? 置位/复位寄存器
GPIOG_BRR 0x14 0x40012014 ? 复位寄存器

4.C语言操作寄存器

(1)ARM是内存与IO统一编址的,所以ARM中的所有外设都是通过寄存器的方式来操作的
(2)每个寄存器都有地址,C语言通过这些地址来操作这些寄存器位,用到的C语言技巧主要是C语言的位操作和C语言指针。
(3)常见面试题:用C语言向内存地址0x30000004写入16
(unsigned int )0x30000004 = 16; 或者:
unsigned int p = (unsigned int )0x30000004; *p = 16;
原始代码:

  1. #define GPIOG_CRL ((unsigned int *)0x40012000)
  2. #define GPIOG_CRH ((unsigned int *)0x40012004)
  3. #define GPIOG_IDR ((unsigned int *)0x40012008)
  4. #define GPIOG_ODR ((unsigned int *)0x4001200C)
  5. #define GPIOG_BSRR ((unsigned int *)0x40012010)
  6. #define GPIOG_BRR ((unsigned int *)0x40012014)
  7. #define RCC_APB2ENR ((unsigned int *)0x40021018)
  8. int main(void)
  9. {
  10. //使能GPIOG
  11. *RCC_APB2ENR = 0x00000100;
  12. // 向CRH寄存器写内容,将GPIOG6-7配置为输出模式
  13. // 推挽输出模式,输出模式为50MHZ
  14. *GPIOG_CRL = 0x33333333;
  15. // 将GPIOG6 GPIOG7设置为1
  16. //*((unsigned int *)GPIOG_ODR) = 0x000000F0;
  17. //通过BSRR寄存器置1
  18. *GPIOG_BSRR = 0x0000000C0;
  19. while(1);
  20. }

代码优化:
gpio.h

  1. #ifndef H_GPIO_G
  2. #define H_GPIO_G
  3. typedef unsigned int u32;
  4. #define PERIPH_BASE 0x40000000
  5. #define GPIOG_BASE (PERIPH_BASE + 0x12000)
  6. #define RCC_APB2ENR ((unsigned int *)0x40021018)
  7. typedef struct
  8. {
  9. u32 CRL; //*
  10. u32 CRH; //*
  11. u32 IDR; //*
  12. u32 ODR; //*
  13. u32 BSRR;
  14. u32 BRR; //*
  15. }GPIOG_TypeDef;
  16. #define GPIOG ((GPIOG_TypeDef *)GPIOG_BASE)
  17. #endif

main.c

  1. #include "gpio.h"
  2. int main(void)
  3. {
  4. //使能GPIOG
  5. *RCC_APB2ENR = 0x00000100;
  6. // 向CRH寄存器写内容,将GPIOG6-7配置为输出模式
  7. // 推挽输出模式,输出模式为50MHZ
  8. GPIOG->CRL = (0x33 << 24);
  9. GPIOG->BSRR = 0x0000000C0;
  10. while(1);
  11. }

闪烁

  1. #include "gpio.h"
  2. void Delay(void);
  3. void Delay(void)
  4. {
  5. unsigned int i = 0, j = 0;
  6. for( i = 0; i < 1000; i++)
  7. {
  8. for( j = 0; j < 2000; j++);
  9. }
  10. }
  11. int main(void)
  12. {
  13. //使能GPIOG
  14. RCC_APB2ENR = 0x00000100;
  15. // 向CRH寄存器写内容,将GPIOG6-7配置为输出模式
  16. // 推挽输出模式,输出模式为50MHZ
  17. GPIOG->CRL = (0x33 << 24);
  18. while(1)
  19. {
  20. GPIOG->BSRR = 0x000000080;
  21. Delay();
  22. GPIOG->BSRR = 0x00000040;
  23. Delay();
  24. GPIOG->BSRR = 0x00ff0000;
  25. Delay();
  26. }
  27. }

5.时钟设置和函数移植

5.1、时钟模块回顾
(1)一个疑惑:前面代码并没有设置时钟为什么能够运行->默认启动就会使用 内部的8MHz
(2)时钟框图
5.2、时钟设置示例代码分析
(1)相关寄存器及定义
(2)代码详解
RCC->CR 就相当于是rRCC_APB2ENR
clock.h

  1. #ifndef __CLOCK_H__
  2. #define __CLOCK_H__
  3. #include "gpio.h"
  4. // 寄存器宏定义
  5. // RCC寄存器基地址为0x40021000
  6. #define RCC_BASE 0x40021000 // RCC部分寄存器的基地址
  7. #define RCC_CR (RCC_BASE + 0x00) // RCC_CR的地址
  8. #define RCC_CFGR (RCC_BASE + 0x04)
  9. #define FLASH_ACR 0x40022000
  10. // 用C语言来访问寄存器的宏定义
  11. #define rRCC_CR (*((volatile unsigned int *)RCC_CR))
  12. #define rRCC_CFGR (*((volatile unsigned int *)RCC_CFGR))
  13. #define rFLASH_ACR (*((volatile unsigned int *)FLASH_ACR))
  14. // 函数作用:时钟源切换到HSE并且使能PLL,将主频设置为72MHz
  15. void Set_SysClockTo72M(void);
  16. #endif

clock.c
如果出错了,通过LED的亮和灭来调试信息

  1. #include "clock.h"
  2. void Set_SysClockTo72M(void)
  3. {
  4. unsigned int rccCrHserdy = 0;
  5. unsigned int rccCrPllrdy = 0;
  6. unsigned int rccCfrSwsPll = 0;
  7. unsigned int faultTime = 0;
  8. rRCC_CR = 0x00000083;
  9. rRCC_CR &= ~(1<<16); // 关闭HSEON
  10. rRCC_CR |= (1<<16); // 打开HSEON,让HSE工作
  11. do
  12. {
  13. rccCrHserdy = rRCC_CR & (1<<17); //检测第17位是否为1
  14. faultTime++;//检测时间
  15. }
  16. while ((faultTime<0x0FFFFFFF) && (rccCrHserdy==0));
  17. if ((rRCC_CR & (1<<17)) != 0)
  18. {
  19. rFLASH_ACR |= 0x10;
  20. rFLASH_ACR |= (0x02);
  21. // 到这里HSE就ready了,下面再去配PLL并且等待他ready
  22. rRCC_CFGR &= (~((0x0f<<4) | (0x07<<8) | (0x07<<11)));
  23. //rRCC_CFGR &= (~(0x3ff<<4));
  24. // AHB和APB2未分频,APB1被2分频,所以最终:AHB和APB2都是72M,APB1是36M
  25. rRCC_CFGR |= ((0x0<<4) | (0x04<<8) | (0x0<<11));
  26. // 选择HSE作为PLL输入并且HSE不分频,所以PLL输入为8M
  27. rRCC_CFGR &= (~((1<<16) | (1<<17))); // 清零bit17和bit16
  28. rRCC_CFGR |= ((1<<16) | (0<<17)); // 置1 bit16
  29. // 设置PLL倍频系数为9
  30. rRCC_CFGR &= (~(0x0f<<18)); // 清零bit18-21
  31. rRCC_CFGR |= (0x07<<18); // 9倍频
  32. // 打开PLL开关
  33. rRCC_CR |= (1<<24);
  34. // do while 循环等待PLL时钟稳定
  35. faultTime = 0;
  36. do
  37. {
  38. rccCrPllrdy = rRCC_CR & (1<<25); //检测第25位是否为1
  39. faultTime++;//检测时间
  40. }
  41. while ((faultTime<0x0FFFFFFF) && (rccCrPllrdy==0));
  42. //while (rccCrPllrdy==0);
  43. if ((rRCC_CR & (1<<25)) == (1<<25))
  44. {
  45. // 到这里说明PLL已经稳定了,可以用了,下面就可以切了
  46. // 切换PLL输出为SYSCLK
  47. rRCC_CFGR &= (~(0x03<<0));
  48. rRCC_CFGR |= (0x02<<0);
  49. faultTime = 0;
  50. do
  51. {
  52. rccCfrSwsPll = rRCC_CFGR & (0x03<<2); //检测第25位是否为1
  53. faultTime++;//检测时间
  54. }
  55. while ((faultTime<0x0FFFFFFF) && (rccCfrSwsPll!=(0x02<<2)));
  56. if ((rRCC_CFGR & (0x03<<2))== (0x02<<2))
  57. {
  58. // 到这里我们的时钟整个就设置好了,可以结束了
  59. }
  60. else
  61. {
  62. // 到这里就说明PLL输出作为SYSCLK不成功
  63. while (1);
  64. }
  65. }
  66. else
  67. {
  68. // 到这里就说明PLL启动时出错了,PLL不能稳定工作
  69. while (1);
  70. }
  71. }
  72. else
  73. {
  74. // HSE配置超时,说明HSE不可用,一般硬件就有问题要去查
  75. while (1);
  76. }
  77. }

6.编程总结

6.1、STM32和51或其他简单单片机的相同
(1)开关环境都是Keil
(2)都是看原理图和数据手册
(3)都是用C语言
6.2、STM32和51或其他简单单片机的不同
(1)工程会更复杂,会用到Keil的一些高级设置
(2)原理图和数据手册比简单单片机更复杂(复杂不是难)
(3)STM32会用到C语言的更多高级特性
6.3、外设编程思路
(1)都是套路
(2)会出现问题,这时候就需要调试能力(不一定非要调试器)
(3)注意熟悉和体会这种套路