关于为什么我们软硬件都要懂?
2010 年,我在华为时,暂时脱产去参与招聘工作;为了为本部门招聘更多的人,争夺 HC 号(招聘入职的名额),所以非常卖力的希望每个来面试的,符合硬杠杠的面试者都能通过层层面试。
华为是如何研发的(21)——面试
因为华为招聘的时候,低级别招聘时有很多硬性条件,比如:学校要满足要求、不能频繁跳槽、不能有留级、复读;要过技术面试、集体面试、心理测评,最终送给终面官做终面;然后送人力资源审核,总部审核。
我们招聘锻炼干部,为了完成招聘任务,都是技面放水、集面指导、心理测评暗示;拼命把 学校满足要求的人送到终面。
有次在青岛的时候,当天通过面试的人很少。终于逮到一个学校勉强满足要求的面试者,技术有点烂。我们一路放水。最终把他送到部门老大那里做终面。
结果,没过一会部门老大出来跟我说:“让他走……”。
我把哪位来面试的小伙子送走了之后,问老大:“刚刚那个硬性条件都满足要求哎,为啥不要啊?”
老大叼根烟,跟我说:“一个搞单片机的,搞三年了的,只会画电路,不会写代码。。。一是学习能力弱、二是不好学、三是现有技能基础太弱。我们招聘过来虽然是来也只是画电路的,但是电路复杂度比简单的单片机系统复杂得多。一个只能画 MCU 电路的,都不知道学习代码,得弱到什么程度。不要不要!”
我当时不理解,觉得领导挑人随心情,不体恤我们这些招聘的锻炼干部的辛苦。过第二天到了济南,老大跟我说:“这些人招聘进来,都是要给你做下属的,如果很难带出来,也是给你自己找麻烦。”
不过现在想来,觉得不无道理。
前期经典文章软硬兼修
我们通过这篇文章,从芯片内部原理、电路设计、软件开发、软件运行,全流程看一下 GPIO 初始化、工作的全过程。帮助很多朋友只做 MCU 硬件的,又很想提升的朋友,进行学习和理解。
【1、熟悉芯片的内部结构和工作原理】
任何开发,我们都应该去看厂家的 Datasheet 和设计参考先,其他的教材、文档、书籍、网站、博客,都是基于原厂的资料进行编辑的。
所以我们应该下载并仔细阅读下面两个文档:
https://www.st.com/resource/en/datasheet/cd00191185.pdf
https://www.st.com/stonline/products/literature/rm/13902.pdf
上面两个链接可以复制到电脑进行下载。
也可以在硬十的论坛的 iBox 专题进行下载:
https://www.hw100k.com/forum.php ... tid=3511&extra=
STM32 实物图:
STM32 的 144 个管脚,除了为芯片供电的电源、GND、时钟、复位管脚之外,几乎所有管脚都可以用作 GPIO
2. STM32,LQFP144PIN 的引脚分布图:
STM32F103ZET6:共 144 个引脚,7 组 IO 口,每组 16 个 IO 口
7*16=112 个 IO 口(这 7 组 IO 口分别为 GPIOA,GPIOB…GPIOG)
例如:PGIOA 包含 PA0,PA1,PA2…PA15,每组 16 个 IO 口。
从硬件上面,按照:A、B、C、D、E、F、G 分为 7 组。
每组有 16 个管脚。
IO 口的基本结构和工作方式
stm32 的 GPIO 的配置模式有好几种,包括:
4 种输入模式
输入浮空
输入上拉
输入下拉
模拟输入
4 种输出模式
开漏输出
开漏复用功能
推挽输出
推挽复用功能
可配置 3 种最大翻转速度
2MHz
10MHz
50MHz
1、模拟输入;
部分管脚可以用作 ADC 的输入管脚,需要通过软件进行配置。
当我们把对应的 GPIO 配置成 ADC 的功能。
则信号接到 GPIO 的管脚,会被 MCU 内部集成的 ADC 进行检测。
从上图我们可以看到,我觉得模拟输入最重要的一点就是,他不经过输入数据寄存器,所以我们无法通过读取输入数据寄存器来获取模拟输入的值,我觉得这一点也是很好理解的,因为输入数据寄存器中存放的不是 0 就是 1,而模拟输入信号不符合这一要求,所以自然不能放进输入数据寄存器。该输入模式,使我们可以获得外部的模拟信号。
如果信号不作为模拟信号输入,可以作为数字信号输入。
数字信号输入时,可以配置上下拉电阻:高阻状态、无上下拉,为浮空输入。
上拉电阻打开,则为上拉输入、如果下拉电阻打开,则为下拉输入。
上拉和下拉部分均为关闭状态(AD 转换 - 模拟量转换为数字量)
施密特触发器为截止状态
通过模拟输入通道输入到 CPU
IO 口外部电压为模拟量(电压形式非电平形式),作为模拟输入范围一般为 0~3.3V
2、 浮空输入;
该输入状态,我的理解是,它的输入完全由外部决定,我觉得在数据通信中应该可以使用该模式。应为在数据通信中,我们直观的理解就是线路两端连接着发送端和接收断,他们都需要准确获取对方的信号电平,不需要外界的干预。所以我觉得这种情况适合浮空输入。比如我们熟悉的 I2C 通信的输入状态。
1)外部通过 IO 口输入电平,外部电平通过上下拉部分(浮空模式下都关闭,既无上拉也无下拉电阻)
2)传输到施密特触发器(此时施密特触发器为打开状态)
3)继续传输到输入数据寄存器 IDR
4)CPU 通过读输入数据寄存器 IDR 实现读取外部输入电平值。在输入浮空模式下可以读取外部输入电平
3、上拉输入;
上拉输入就是在输入电路上使用了上拉电阻。这种模式的好处在于我们什么都不输入时,由于内部上拉电阻的原因,我们的处理器会觉得我们输入了高电平,这就避免了不确定的输入。这在要求输入电平只要高低两种电平的情况下是很有用的。
和输入浮空模式相比较,不同之处在于内部有一个上拉电阻连接到 VDD(输入上拉模式下,上拉电阻开关接通,阻值约 30-50K)
外部输入通过上拉电阻,施密特触发器存入输入数据寄存器 IDR,被 CPU 读取。
4、下拉输入;
和上拉输入类似,不过下拉输入时,在外部没有输入时,我们的处理器会觉得我们输入了低电平。
和输入浮空模式相比较,不同之处在于内部有一个下拉电阻连接到 VSS(输入下拉模式下,下拉电阻开关接通,阻值约 30-50K)
外部输入通过下拉电阻,施密特触发器存入输入数据寄存器 IDR,被 CPU 读取
5、开漏输出;
开漏输出,输出端相当于三极管的集电极,所以适合与做电流驱动的应用。要得到高电平,需要上拉电阻才可以。(例如模拟软件实现 I2C 的输出)
1,CPU 写入位设置 / 清楚寄存器 BSRR,映射到输出数据寄存器 ODR
2,联通到输出控制电路(也就是 ODR 的电平)
3,ODR 电平通过输出控制电路进入 N-MOS 管
-ODR 输出 1:
N-MOS 截止,IO 端口电平不会由 ODR 输出决定,而由外部上拉 / 下拉决定。
在输出状态下,输出的电平可以被读取,数据存入输入数据寄存器,由 CPU 读取,实现 CPU 读取输出电平。
所以,当 N-MOS 截止时,如果读取到输出电平为 1,不一定是我们输出的 1,有可能是外部上拉产生的 1。
-ODR 输出 0:
N-MOS 开启,IO 端口电平被 N-MOS 管拉倒 VSS,使 IO 输出低电平。
此时输出的低电平同样可以被 CPU 读取到。
6、 推挽输出;
推挽输出使用了推挽电路,结合推挽电路的特性,它是由两个 MOSFET 组成,一个导通的同时,另外一个截至,两个 MOSFET 分别连接高低电平,所以哪一个导通就会输出相应的电平。推挽电路速度快,输出能力强,直接输出高电平或者低电平。
与开漏输出模式唯一的区别在于输出控制电路之前电平的来源
开漏输出模式的输出电平是由 CPU 写入输出数据寄存器控制的
开漏推挽输出模式的输出电平是由复用功能外设输出决定的
其他与开漏输出模式相似:
控制电路输出为 1:N-MOS 截止,IO 口电平由外部上拉 / 下拉决定
控制电路输出为 0:N-MOS 开启,IO 口输出低电平
7、复用功能的开漏输出;
我们使用了某个硬件接口,但是这个接口需要开漏输出,则 GPIO 处于这个状态。例如我们使用片内外设功能(I2C 的 SCL,SDA),即我们口头说的硬件 I2C,需要输出是个开漏,且使用了 MCU 的 GPIO 复用功能。
与开漏输出相比较:
输出控制寄存器部分相同
输出驱动器部分加入了 P-MOS 管部分
当输出控制电路输出 1 时:
P-MOS 管导通 N-MOS 管截止,被上拉到高电平,IO 口输出为高电平 1
当输出控制电路输出 0 时:
P-MOS 管截止 N-MOS 管导通,被下拉到低电平,IO 口输出为低电平 0
同时 IO 口输出的电平可以通过输入电路读取
8、 复用推挽输出;
我们使用了某个硬件接口,但是这个接口需要推挽输出,则 GPIO 处于这个状态。例如我们使用片内外设功能(SPI 接口的管脚)。
与推挽输出模式唯一的区别在于输出控制电路之前电平的来源
开漏输出模式的输出电平是由 CPU 写入输出数据寄存器控制的
开漏推挽输出模式的输出电平是由复用功能外设输出决定的
注:推挽输出和开漏输出的区别
推挽输出:
可以输出强高 / 强低电平,可以连接数字器件
开漏输出:
只能输出强低电平(高电平需要依靠外部上拉电子拉高),适合做电流型驱动,吸收电流能力较强(20ma 之内)
总结一下,在 STM32 中选用 IO 模式:
(1)模拟输入 _AIN ——应用 ADC 模拟输入
(2)浮空输入 _IN_FLOAtiNG ——浮空输入,可以做 KEY 识别,RX1
(3)带上拉输入 _IPU——IO 内部上拉电阻输入
(4)带下拉输入 _IPD—— IO 内部下拉电阻输入
(5)开漏输出 _OUT_OD ——IO 输出 0 接 GND,IO 输出 1,悬空,需要外接上拉电阻,才能实现输出高电平。当输出为 1 时,IO 口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样 IO 口也就可以由外部电路改变为低电平或不变。可以读 IO 输入电平变化,实现 C51 的 IO 双向功能
(6)推挽输出 _OUT_PP ——IO 输出 0- 接 GND, IO 输出 1 - 接 VCC,读输入值是未知的
(7)复用功能的推挽输出 _AF_PP ——片内外设功能(I2C 的 SCL,SDA)
(8)复用功能的开漏输出 _AF_OD——片内外设功能(TX1,MOSI,MISO.SCK.SS)
【2、STM32-IO 口相关寄存器】
每组 GPIO 包含系列 7 个寄存器(7 组 GPIO 共包含 7*7=49 个寄存器)
两个 32 位配置寄存器
GPIOx_CRL 低 16 位
GPIOx_CRH 高 16 位
两个 32 位数据寄存器
GPIOx_IDR 输入数据寄存器
GPIOx_ODR 输出数据寄存器
一个 32 位置位 / 复位寄存器
GPIOx_BSRR
一个 16 位复位寄存器
GPIOx_BRR
一个 32 位锁定寄存器
GPIOx_LCKR
六,STM32-IO 口相关寄存器讲解
1,端口配置寄存器:
STM32 每组 GPIO 位 16 个 IO 口,每 4 位控制一个 IO 口,所以 32 位控制 8 个 IO 口
分为低 16 位:GPIOx_CRL 和高 16 位:GPIOx_CRH 共 32 位控制一组 GPIO 的 16 个 IO 口
如图:以端口配置寄存器低 16 位为例,每四位控制一个 IO 口(高 16 位同理)
MODEx 的 2 位 : 配置 IO 口输出 / 输出模式(1 种输出+3 种不同速度的输出模式)
CNFx 的 2 位 : 配置 IO 口输入 / 输出状态下(由 MODEx 控制)的输入 / 输出模式
以 GPIOA_CRL 为例,配置 IO 口 PA0 -> MODE0=00(输入模式) CNF0=10(上拉 / 下拉输入模式)
此种配置下到底是上拉还是下拉输入模式还需由 ODR 寄存器决定
关于上拉 / 下拉的控制我们将在下面-数据寄存器-中介绍 ODR 输出寄存器时详细说明
2,数据寄存器(以输入数据寄存器 GPIOx_IDR 为例)
每一组 IO 口都具有一个 GPIOx_IDR 的 32 位寄存器(实际只使用低 16 位,高 16 位保留),即 16 位控制 16 个 IO 口,每一位控制一个
如图:IDR 寄存器共 32 位,0~15 位代表一组 IO 口 16 个 IO 当前值
这里我们已经了解了输入 / 输出数据寄存器,现在说下上面提到的问题:
当 IO 口配置为输入模式且配置为上拉 / 下拉输入模式(即 MODEx=00 CNFx=10 时),ODR 决定到底是上拉还是下拉
1)当输出模式时,ODR 为输出数据寄存器
2)当输入模式时,ODR 用作区分当前位输入模式到底是上拉输入(ODRx=0)还是下拉输入(ODRx=1)
3,端口位设置 / 清除寄存器(GPIOx_BSRR)
BSRR 寄存器作用:
BSRR 寄存器为 32 位寄存器,低 16 位 BSx 为设置为(1 设置 0 不变),高 16 位 BRx 为重置位(1:清除 0:不变)
当然,最终的目的还是通过 BSRR 间接设置 ODR 寄存器,改变 IO 口电平
4,端口位清除寄存器(GPIOx_BRR)
GPIOx_BRR 寄存器作用同 GPIOx_BSRR 寄存器高 16 位
一般我们使用 BSRR 低 16 位和 BRR 的低 16 位(STM32F4 系列取消了 BSRR 的高 16 位)
5,锁存寄存器:使用较少暂不分析
七,端口的复用和重映射
1,端口的复用:
大部分 IO 口可复用为外部功能引脚,参考芯片数据手册(IO 口复用和重映射)
例如:STM32F103ZET6 的 PA9 和 PA10 引脚可复用为串口发送和接收功能引脚,也可复用为定时器 1 的通道 2 和通道 3
端口复用的作用:最大限度的利用端口资源
2,端口的重映射:
串口 1 默认引脚是 PA9,PA10 可以通过配置重映射映射到 PB6,PB7
端口重映射的作用:方便布线
3,STM32 所有的 IO 口都可作为中断输入(51 单片机只有 2 个端口可以作为外部中断输入)
【3、GPIO 软件运行的过程】
首先,我们打开 iBox 开发板的例程 LED_DEMO(下载地址详见上期内容:从原理图 PCB 到移植 RTOS【细说 STM32】【二】新建一个自己的工程),点击软件上方的“Start/stop Debug Session”按钮,如下图:
程序首先从主函数开始。我们点击左上角的 step 或快捷键 F11,
就会发现左边黄色的箭头移动到主函数位置:
继续 F11,我们发现黄色箭头移至下图所示位置:
上面的函数是使能 GPIOE 端口的时钟的。GPIOE 是属于 APB2 这条高速总线上的,所以用函数 RCC_APB2PeriphClockCmd(xxx, xxx) 来启动对应的 I/O 端口。GPIO 只有在时钟上得以启动,我们才能使用它们。
我们再次点击,发现进入了 RCC_APB2PeriphClockCmd ( xxx , xxx)函数里面。
assert_param(IS_RCC_APB2_PERIPH(RCC_APB2Periph));
assert_param(IS_FUNCTIONAL_STATE(NewState));这两行是用来检测形参是否是指定范围内的值。继续 F11 会发现黄色光标跳到了"RCC->APB2ENR |= RCC_APB2Periph;"。这里就是把 RCC_APB2Periph_GPIOE 的值赋给 RCC_APB2ENR。在此暂时不对此函数做详细介绍。
接下来的四次单步运行,我们会发现程序跳回了主函数。黄色箭头会依次在下图中四行代码中移动。
我们逐行分析:
第一行的作用是选择 IO 端口的工作方式。由于驱动 LED 需要较大电流,所以此处采用推挽输出的方式。如果想更改为其他方式,只需将上图等号后面的” GPIO_Mode_Out_PP”改为其他工作方式即可。
GPIO_Mode_Out_PP 推挽输出
GPIO_Mode_Out_OD 开漏输出
GPIO_Mode_AF_PP 复用推挽输出
GPIO_Mode_AF_OD 复用开漏输出
GPIO_Mode_AIN 模拟输入
GPIO_Mode_IN_FLOATING 浮空输入
GPIO_Mode_IPD 下拉输入
GPIO_Mode_IPU 上拉输入
第二行的作用是选择 IO 端口号。此处我们选择了 GPIOE 的 9、10、12 口。GPIO_Pin_x 其 x 的值可以是 0~15,如果我们同时选择多个 IO 口,可以用上图方式用“|”隔开,也可以用 GPIO_Pin_All 来选择 GPIOE 组下的所有端口。
第三行的作用是设置 IO 端口的速度。端口的速度只有在 IO 端口被设置为输出的时候才需要设置。如果为输入模式则不需要设置。端口速度可以 2/10/50MHz。上面我们设置 IO 端口工作方式为推挽输出,所以此处需要设置 IO 端口的速度。
第四行的作用是运行 GPIO 的初始化库函数。把上面三条的设置内容写入到 IO 端口对应的寄存器当中。其中第一个参数是我们要写入的哪一组端口。第二个参数就是对这一组端口的设置内容,也就是我们刚才设置的结构体内容。最终完成三个端口的初始化。
再次单步执行,我们发现此程序跳到了 GPIO_Init()函数。
此函数后续我们会详细介绍,此处暂不做介绍。
直接 step out(Ctrl+F11)跳到下一步。我们发现程序回到了主函数如下图所示位置。
此处用到了函数 GPIO_SetBits。此函数中只有两个参数:第一个参数是选择 GPIO 的组(此处我们选择了 GPIOE);第二个参数是选择 GPIO 的 Pin(此处我们选择了 9、10、12)。此函数是专门用来将 IO 口置位(输出高电平)。下图代码将 GPIOE 组下的 9、10、12 号端口设为高电平。函数执行至此,我们可以观察到 iBox 上的三个指示灯全部亮了起来。
与 GPIO_SetBits 对应的函数为 GPIO_ResetBits。此函数的功能为清楚指定数据端口位,也就是用来将 IO 口清零的函数。使用方法与 GPIO_SetBits 相同。操作 IO 口的方法还有很多种,后面我们会逐步介绍。
继续单步运行程序,发现黄色箭头跳转到下图所示位置:
GPIOx->BSRR 是端口位设置 / 清除寄存器。其作用为将端口位设置或清除。
继续单步运行,程序再次跳回主函数如下图所示的位置:
其中“for(; ;)”可以理解为“while(1)”。相比之下 for 式死循环更加高效一些。
即不设初值,不判断条件,循环变量不增值,无终止的循环,程序会一直执行大括号里面的内容。
上图大括号里面的第一行用到了 GPIO_SetBits,后面的两个参数分别为 GPIO_LED_PORT 和 GPIO_LED_ALL 我们右键选择“Go To Definition Of 'GPIO_LED_PORT',就会跳转到下图所示的位置:
不难看出,这里定义了 GPIOE 为 GPIO_LED_PORT;定义了 GPIO_Pin_9、GPIO_Pin_10、GPIO_Pin_11、GPIO_Pin_12 为 GPIO_LED_ALL。
这样一来,我们就很容易理解这四条语句的含义了:首先用 GPIO_SetBits 将 GPIOE 组下的 9、10、11、12 号端口置高,延时一段时间后,用 GPIO_ResetBits 将 GPIOE 组下的 9、10、11、12 号端口清零,再延时一段时间,由于是在 for(;;)中执行,程序会一直在这里循环,这样就实现了 LED 闪灯的效果。
|