这里仅总结一下IO控制相关及这种总线等  ~持续更新 第14部的啊   

一、STM32的GPIO电路原理

GPIO是通用输入/输出端口的简称,是STM32可控制的引脚。GPIO的引脚与外部硬件设备连接,可实现与外部通讯、控制外部硬件或者采集外部硬件数据的功能。

    STM32F103ZET6芯片为144脚芯片,包括7个通用目的的输入/输出口(GPIO)组,分别为GPIOA、GPIOB、GPIOC、GPIOD、GPIOE、GPIOF、GPIOG,同时每组GPIO口组有16个GPIO口。通常简略称为PAx、PBx、PCx、PDx、PEx、PFx、PGx,其中x为0-15。

    STM32的大部分引脚除了当GPIO使用之外,还可以复用为外设功能引脚,比如串口。

GPIO基本结构

    每个GPIO内部都有这样的一个电路结构,这个结构在本文下面会具体介绍。

嵌入式分享~IO相关14_数据

  保护二极管:IO引脚上下两边两个二极管用于防止引脚外部过高、过低的电压输入。当引脚电压高于VDD时,上方的二极管导通;当引脚电压低于VSS时,下方的二极管导通,防止不正常电压引入芯片导致芯片烧毁。但是尽管如此,还是不能直接外接大功率器件,须加大功率及隔离电路驱动,防止烧坏芯片或者外接器件无法正常工作。

    P-MOS管和N-MOS管:由P-MOS管和N-MOS管组成的单元电路使得GPIO具有“推挽输出”和“开漏输出”的模式。这里的电路会在下面很详细地分析到。

    TTL肖特基触发器:信号经过触发器后,模拟信号转化为0和1的数字信号。但是,当GPIO引脚作为ADC采集电压的输入通道时,用其“模拟输入”功能,此时信号不再经过触发器进行TTL电平转换。ADC外设要采集到的原始的模拟信号。

    这里需要注意的是,在查看《STM32中文参考手册V10》中的GPIO的表格时,会看到有“FT”一列,这代表着这个GPIO口时兼容3.3V和5V的。如果没有标注“FT”,就代表着不兼容5V。

STM32的GPIO工作方式

    GPIO支持的输入输出模式:

  • GPIO_Mode_AIN 模拟输入
  • GPIO_Mode_IN_FLOATING 浮空输入
  • GPIO_Mode_IPD 下拉输入
  • GPIO_Mode_IPU 上拉输入
  • GPIO_Mode_Out_OD 开漏输出
  • GPIO_Mode_Out_PP 推挽输出
  • GPIO_Mode_AF_OD 复用开漏输出
  • GPIO_Mode_AF_PP 复用推挽输出

    每个I/O口可以自由编程,但I/O口寄存器必须按32位字被访问。

    下面将具体介绍GPIO的这八种工作方式:

浮空输入模式

嵌入式分享~IO相关14_数据_02

   浮空输入模式下,I/O端口的电平信号直接进入输入数据寄存器。也就是说,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的。

上拉输入模式

嵌入式分享~IO相关14_数据_03

  上拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在高电平;并且在I/O端口输入为低电平的时候,输入端的电平也还是低电平。

下拉输入模式

嵌入式分享~IO相关14_嵌入式硬件_04

下拉输入模式下,I/O端口的电平信号直接进入输入数据寄存器。但是在I/O端口悬空(在无信号输入)的情况下,输入端的电平可以保持在低电平;并且在I/O端口输入为高电平的时候,输入端的电平也还是高电平。

模拟输入模式

嵌入式分享~IO相关14_输出模式_05

  模拟输入模式下,I/O端口的模拟信号(电压信号,而非电平信号)直接模拟输入到片上外设模块,比如ADC模块等等。

开漏输出模式

嵌入式分享~IO相关14_数据_06

 开漏输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经N-MOS管,最终输出到I/O端口。这里要注意N-MOS管,当设置输出的值为高电平的时候,N-MOS管处于关闭状态,此时I/O端口的电平就不会由输出的高低电平决定,而是由I/O端口外部的上拉或者下拉决定;当设置输出的值为低电平的时候,N-MOS管处于开启状态,此时I/O端口的电平就是低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,I/O端口的电平不一定是输出的电平。

开漏复用输出模式

嵌入式分享~IO相关14_数据_07

  开漏复用输出模式,与开漏输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。

推挽输出模式

嵌入式分享~IO相关14_嵌入式硬件_08

   推挽输出模式下,通过设置位设置/清除寄存器或者输出数据寄存器的值,途经P-MOS管和N-MOS管,最终输出到I/O端口。这里要注意P-MOS管和N-MOS管,当设置输出的值为高电平的时候,P-MOS管处于开启状态,N-MOS管处于关闭状态,此时I/O端口的电平就由P-MOS管决定:高电平;当设置输出的值为低电平的时候,P-MOS管处于关闭状态,N-MOS管处于开启状态,此时I/O端口的电平就由N-MOS管决定:低电平。同时,I/O端口的电平也可以通过输入电路进行读取;注意,此时I/O端口的电平一定是输出的电平。

推挽复用输出模式

嵌入式分享~IO相关14_单片机_09

  推挽复用输出模式,与推挽输出模式很是类似。只是输出的高低电平的来源,不是让CPU直接写输出数据寄存器,取而代之利用片上外设模块的复用功能输出来决定的。

总结与分析

什么是推挽结构和推挽电路?

    推挽结构一般是指两个参数相同的三极管或MOS管分别受两互补信号的控制,总是在一个三极管或MOS管导通的时候另一个截止。高低电平由输出电平决定。

    推挽电路是两个参数相同的三极管或MOSFET,以推挽方式存在于电路中,各负责正负半周的波形放大任务。电路工作时,两只对称的功率开关管每次只有一个导通,所以导通损耗小、效率高。输出既可以向负载灌电流,也可以从负载抽取电流。推拉式输出级既提高电路的负载能力,又提高开关速度。

开漏输出和推挽输出的区别?

    开漏输出:只可以输出强低电平,高电平得靠外部电阻拉高。输出端相当于三极管的集电极。适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内);

推挽输出:可以输出强高、低电平,连接数字器件。

    关于推挽输出和开漏输出,最后用一幅最简单的图形来概括:

嵌入式分享~IO相关14_单片机_10

  该图中左边的便是推挽输出模式,其中比较器输出高电平时下面的PNP三极管截止,而上面NPN三极管导通,输出电平VS+;当比较器输出低电平时则恰恰相反,PNP三极管导通,输出和地相连,为低电平。右边的则可以理解为开漏输出形式,需要接上拉。

在STM32中选用怎样选择I/O模式?

  • 浮空输入_IN_FLOATING ——浮空输入,可以做KEY识别,RX1
  • 带上拉输入_IPU——IO内部上拉电阻输入
  • 带下拉输入_IPD—— IO内部下拉电阻输入
  • 模拟输入_AIN ——应用ADC模拟输入,或者低功耗下省电
  • 开漏输出_OUT_OD ——IO输出0接GND,IO输出1,悬空,需要外接上拉电阻,才能实现输出高电平。当输出为1时,IO口的状态由上拉电阻拉高电平,但由于是开漏输出模式,这样IO口也就可以由外部电路改变为低电平或不变。可以读IO输入电平变化,实现C51的IO双向功能
  • 推挽输出_OUT_PP ——IO输出0-接GND, IO输出1 -接VCC,读输入值是未知的
  • 复用功能的推挽输出_AF_PP ——片内外设功能(I2C的SCL、SDA)
  • 复用功能的开漏输出_AF_OD——片内外设功能(TX1、MOSI、MISO.SCK.SS)
二、单片机之间传输数据的一些方法

MCU之间通信的主要方式

1、采用硬件UART进行异步串行通信

这是一种占用口线少,有效、可靠的通信方式;但遗憾的是许多小型单片机没有硬件UART,有些也只有1个UART,如果系统还要与上位机通信的话,硬件资源是不够的。这种方法一般用于单片机有硬件UART且不需与外界进行串行通信或采用双UART单片机的场合。

2、采用片内SPI接口或I2C总线模块串行通信形式

SPI/I2C接口具有硬件简单、软件编程容易等特点,但目前大多数低端的单片机不具备硬件SPI/I2C外设。

3、可以利用软件模拟SPI/I2C模式通信

这种方式很难模拟从机模式,通信双方对每一位要做出响应,通信速率与软件资源的开销会形成一个很大的矛盾,处理不好会导致系统整体性能急剧下降。这种方法只能用于通信量极少的场合。

4、口对口并行通信

利用单片机的口线直接相连,加上1~2条握手信号线。这种方式的特点是通信速度快,1次可以传输4位或8位,甚至更多,但需要占用大量的口线,而且数据传递是准同步的。在一个单片机向另一个单片机传送1个字节以后,必须等到另一个单片机的接收响应信号后才能传送下一个数据。一般用于一些硬件口线比较富裕的场合。

5、利用双口RAM作为缓冲器通信

这种方式的最大特点就是通信速度快,两边都可以直接用读写存储器的指令直接操作;但这种方式需要大量的口线,而且双口RAM的价格很高,一般只用于一些对速度有特殊要求的场合。

总  结

从上面几种方案来看,各种方法对硬件都有很大的要求与限制,特别是难以在功能简单的单片机上实现,因此寻求一种简单、有效的,能在各种单片机之间通信的方法具有重要的意义。③、④方案中,双方单片机要传递的每一位或每一个字节做出响应,通信数据量较大时会耗费大量的软件资源,这在一些实时性要求高的地方是不允许的。

针对这一问题,假设在单片机之间增加1个数据缓冲器,大批数据先写入缓冲区,然后再让对方去取,各个单片机对数据缓冲器都是主控模式,这样必然会大大提高通信效率。

谈到数据缓冲,我们马上会想到并行RAM,但是并行RAM需要占用大量的口线(数据线+地址线+读写线+片选线+握手线),一般在16条以上。这是一个让人望而生畏的数字,而且会大大增加PCB面积并给布线带来一定的困难,极少有人采用这种方式。

串行接口的RAM在市场上很少见,不但难以买到而且价格很高。移位寄存器也可以做数据缓冲器,但目前容量最大的也只128位,因为是“先进先出”结构,所以不管传递数据多少,接收方必须移完整个寄存器,灵活性差而且大容量的移位寄存器也是少见难买的。一种被称为“铁电存储器”芯片的出现,给我们带来了解决方法。

采用铁电存储器

利用铁电存储器作为数据缓冲器的通信方式,铁电存储器是美国Ramtran公司推出的一种非易失性存储器件,简称FRAM。

与普通EEPROM、Flash-ROM相比,它具有不需写入时间、读写次数无限,没有分布结构可以连续写放的优点,因此具有RAM与EEPROM的双得特性,而且价格相对较低。

现在大多数的单片机系统配备串行EEPROM(如24CXX、93CXX等)用来存储参数。如果用1片FRAM代替原有EEPROM,使它既能存储参数,又能作串行数据通信的缓冲器。

嵌入式分享~IO相关14_输出模式_11

2个(或多个)单片机与1片FRAM接成多主-从的I2C总线方式,增加几条握手线,即可得到简单高效的通信硬件电路。

嵌入式分享~IO相关14_单片机_12

在软件方面,只要解决好I2C多主-从的控制冲突与通信协议问题,即可实现简单、高效、可靠的通信了。

通讯实例

实例(双单片机结构,多功能低功耗系统)

1 硬件

W78LE52与EMC78P458组成一个电池供电、可远程通信的工业流量计。

78P458采用32.768kHz晶振,工作电流低,不间断工作,实时采集传感器的脉冲及温度、压力等一些模拟量。

W78LE52采用11.0592MHz晶振,由于它的工作电流较大,采用间断工作,负责流量的非线性校正、参数输入、液晶显示、与上位机通信等功能,它的UART用于远程通信。

2个单片机共用1片I2C接口的FRAM(FM24CL16)组成二主一从的I2C总线控制方式,W78LE52的P3.5、P3.2分别与78P458的P51、P50连接作握手信号线A与B。

我们把握手线A(简称A线)定义为总线控制、指示线,主要用于获取总线控制权与判别总线是否“忙”;握手线B(简称B线)定义为通知线,主要用于通知对方取走数据。

2 I2C总线仲裁

由于我们采用的是二主一从的I2C总线方式,因此防止2个主机同时去操作从机(防冲突)是一个非常重要的问题。

带有硬件I2C模块的器件一般是这样的,器件内部有1个总线仲裁器与总线超时定时器:当总线超时定时器超时后指示总线空闲,这时单片机可以发出获取总线命令,总线仲裁器通过一系列操作后确认获取总线成功或失败;超时定时器清零,以后的每一个SCL状态变化对总线所有主机的超时定时器进行清零,以防止它溢出,指示总线正处于“忙”状态,直到一个主机对总线控制结束不再产生SCL脉冲;超时定时器溢出,总线重新回到“空闲”状态。

但是目前大多数单片机没有配备硬件I2C模块,而且当2个主机的工作频率相差较大时,超时定时器定时值只能设为较大的值,这样也会影响总线的使用效率。

下面介绍一种用软件模拟I2C总线仲裁的方式(I2C读写操作程序的软件模拟十分多见,这里不再多述)

用1条握手线A,当A线高电平时,指示总线空闲;当其中一个主机要获取总线控制权时,先查询总线是否空闲,“忙”则退出,空闲则向A线发送一个测试序列(如:1000101011001011),在每次发送位“1”后读取的A线状态。

如果读取状态为“0”,马上退出,说明有其它器件已经抢先获取总线;如果一个序列读取的A线状态都正确,则说明已成功获得总线控制权,这时要拉低A线以指示总线“忙”,直到读写高A线,使总线回到“空闲”状态。不同的主机采用不同的测试序列,或产生随机测试序列,测试序列长度可以选得长一些,这样可以增加仲裁的可靠性。

3 通信协议

一个可靠通信体系,除了好的硬件电路外,通信协议也至关重要。在单片机系统RAM资源与执行速度都非常有限的情况下,一个简捷有效的协议是非常重要的。下面具体介绍一种比较适用于单片机通信的协议,数据以包的形式传送。

数据包结构:

① 包头——指示数据包的开始,有利于包完整性检测,有时可省略;

② 地址——数据包要传送的目标地址,若只有双机通信或硬件区分地址可以省略;

③ 包长度——指示整个数据包的长度;

④ 命令——指示本数据包的作用;

⑤ 参数——需要传送的数据与参数;

⑥ 校验——验证数据包的正确性,可以是和校验、异或校验、CRC校验等或者是它们的组合;

⑦ 包尾——指示数据包的结尾,有利于包完整性检测,有时可省略。

4 通信流程

首先,要在FRAM里划分好各个区域,各个单片机的参数区、数据接收区等。

然后,单片机可以向另一个单片机发送数据包,发送完毕之后通过向握手线B发送1个脉冲通知对方取走数据;接收方读取数据并进行处理后,向FRAM内发送方的数据接收区写入回传数据或通信失败标志,再向握手线B发送1个脉冲回应发送方。

如果需要单片机2发送的话,只需交换一下操作过程即可。

项目小结

通过实践可知,以上方法是可行的。与其它方法相比具有发下优点:

①  简单

占用单片机口线少(SCL、SDA、握手线A、握手线B)。

②  通用

软件模拟I2C主机方式,可以在任何种类的单片机之间通信。

③  高效

由于采用数据缓冲,可以在不同时钟频率、不同速度的单片机之间通信;读写数据时,可以I2C总线的最高速度进行,可以实现1次传送大量数据;在一个单片机向FRAM传送数据时,另一个单片机无须一一作出响应或等待,可以进行其它程序操作,提高软件工作效率。

④  灵活

通信硬件接口对于各个单片机是对等的,通过软件配置,每个单片机既可以根据需要主动发送通信,也可以只响应其它单片机的呼叫。

⑤容易扩展

通过增加地址识别线,修改通信协议,即可做到多机通信。

以下是需要注意的地方:

①  为了提高通信效率,握手线B最好使用中断端口,负脉冲宽度一定要满足速度较低单片机中断信号要求。如果没有中断的话应增加1条口线,用改变端口状态的方法通知对方,等待对方查询,而不是负脉冲。

②  向对方发送负脉冲时,应屏蔽自己的中断。

③  由于参数与通信缓冲区同时设在同一片FRAM内,要避免对参数部分的误操作。一个较好的解决办法是把参数存放在地址的后半部分(A2=1),在进行通信操作时,把FRAM的WP引脚拉高(地址在后半部分的单元写保护),这样可以有效地防止测验时对参数区误操作。

④  由于I2C总线在一个时间段内只有1个主机和1个从机,所以当1个单片机正在写通信数据时,另一个单片机是不能对FRAM进行操作的。如果需要实时、频繁地读取FRAM中参数的话,请预先将参数读入RAM单元使用或另外增加专门存放参数的芯片。

三、STM32实用串口控制平台

玩过Linux的朋友, 是不是对Linux无所不能的串口Shell命令控制台羡慕不已, 要是自己做的STM32F系列低档次的MCU也有这种控制交互能力, 会给调试/维护和配置省下多少麻烦事呀, 比如启动/关闭调试或自检模式, 打印调试信息, 配置系统参数, 传输文件等等, 也有相当多的朋友凭借自己出色的编程能力可以实现这些功能, 这里提出我的这个解决方案, 以作交流.

本平台(xc_shell)具备以下性能特点:

大量主要代码, 和具体硬件无关, 移植性强,代码文件少.

只有在处理用户的输入命令时, 才占用CPU资源, 且代码可裁剪到1KB SRAM和4KB Flash;

用户可以非常灵活的添加按模板编写的命令脚本文件, 自定义扩张能力强.

支持操作系统和非操作系统两种场景应用.

支持Ymodem文件传输协议

支持将Flash的扇区开辟为参数区, 可实现本地/远程升级。

实用Led灯信号管理, 可将65535虚拟信号灯选择输出到1个实体LED灯上, 调试时序和状态非常有用

拥有基础的LED管理, 调试模式设置, 命令帮助指令, 复位指令等基础功能

功能越多设计会越复杂, 为了解释清楚代码, 先向大家解释一下以上功能的基础实现原理, 并提供一个最小的的源码工程。

#xc_shell平台介绍

 如何实现硬件无关

类比Linux会发现, 设备的硬件接口往往会被虚拟成一个文件(驱动), 而Linux内核完全与硬件系统无任何字节关联, 不同平台驱动不同而已, 故而本xc_shell的串口驱动也采用了相似的思路:

1) 串口驱动用一个结构体描述, 这样只需在xc_shell.c中用指针指向这个TTYx_HANDLE结构体对象就可以将串口(tty)硬件与内核联系在一起, 聪明的朋友可能会想到, 假如我将带网络的开发板按此结构体,虚拟一个TTY对象, 岂不是就可以实现一个网络远程控制台了!  这点确实是可以的!

2) 当然诸如多TTY串口实现接口互换等, 都是一个指针和step2中的注入回调处理交换的问题。

3)用户在使用api_TxdFrame或api_TxdByte时”bsp_ttyX.c“,会驱动具体MCU的串口将数据发送出去, 收到一帧数据后,若用户设置了inj_RcvFrame回调处理方法,则会在中断中执行用户的回调处理。

/*---------------------* 
*     指正函数定义
*----------------------*/
typedef void    (*pvFunDummy)(void);
 
//输入整行,输出逻辑
typedef void    (*pvFunVoid) (void);
typedef void    (*pvFunBool) (bool     bVal);
typedef void    (*pvFunChar) (uint8_t  cVal);
typedef void    (*pvFunShort)(uint16_t sVal);
typedef void    (*pvFunWord) (uint32_t wVal);
 
//输入整行,输出逻辑
typedef bool    (*pbFunVoid) (void);
typedef bool    (*pbFunBool) (bool     bVal);
typedef bool    (*pbFunChar) (uint8_t  cVal);
typedef bool    (*pbFunShort)(uint16_t sVal);
typedef bool    (*pbFunWord) (uint32_t wVal);
 
//输入整形指针,输出逻辑
typedef bool    (*pbFun_pVoid) (void * pVoid);
typedef bool    (*pbFun_pChar) (uint8_t  * pStr);
typedef bool    (*pbFun_pShort)(uint16_t * pShor);
typedef bool    (*pbFun_pWord) (uint32_t * pWord);
 
//输入数据帧,输出逻辑
typedef bool    (*pbFun_Buffx)(void * pcBuff, uint16_t len );
typedef bool    (*pbFun_Bytex)(uint8_t * pcByte, uint16_t len );
/*---------------------* 
*    TTYx 句柄结构
*----------------------*/
typedef struct TTYx_HANDLE_STRUCT 
{
    const char  * const name;       //驱动器名
    const uint16_t      rxSize;     //接收大小
    const uint16_t      txSize;     //发送大小
    
    //------------------------------------------------------
    //step1: 用户可用API
    const pvFunWord     init;           //初始化.
    const pbFun_Bytex   api_TxdFrame;   //发送数据帧. (发送帧)
    const pbFunChar     api_TxdByte;    //发送数据字节
    
    //------------------------------------------------------
    //step2: 注入回调函数
    pbFun_Bytex         inj_RcvFrame;   //(ISR)接收数据帧. (接收帧)
    pvFunDummy          inj_TxdReady;   //(ISR)发送完毕回调
    
    //------------------------------------------------------
    //step3: 接收回调函数
    struct TTYx_HANDLE_STRUCT * pvNext; //连接到下一个指令 
}TTYx_HANDLE;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.

可注入的命令脚本(CLI)实现:

命令CLI也是一个结构体对象:

/*---------------------* 
*       CLI指令
*----------------------*/
typedef struct
{
 const char * const  pcCmdStr;     //指令字符串(只能为小写字母)
 const char * const  pcHelpStr;     //指令描述,必须以:"\r\n结束". 比如:"help: Returns a list\r\n".
 const pFunHook      pxCmdHook;     //指向回调函数的指针,处理成功返回真否者返回0;
 uint8_t             ucExpParam;     //指令期望的参数个数
 const MEDIA_HANDLE *phStorage;      //指向存储介质,没有的话填充NULL  
}Cmd_Typedef_t;
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

各位朋友可能会使用到非常多的自定义CLI命令, 格式诸如这个网卡的命令:

const Cmd_Typedef_t CLI_WizMsg=
{
    //识别关键字
    .pcCmdStr   = "wiz",
    //帮助内容
    .pcHelpStr  =
    "[WIZ contorls]\r\n"
 " wiz help\r\n"
 " wiz rd info\r\n"
 " wiz reset\r\n"
 " wiz wr ip <D0>.<D1>.<D2>.<D3>\r\n"
 " wiz wr mask <D0>.<D1>.<D2>.<D3>\r\n"
 " wiz wr way <D0>.<D1>.<D2>.<D3>\r\n"
 " wiz wr mac <H0>-<H1>-<H2>-<H3>-<H4>-<H5>\r\n"
 " wiz wr port <udp> <bak> <vol> <pic>\r\n"
 " wiz wr sip <D0>.<D1>.<D2>.<D3> <port>\r\n"
 " wiz wr cip <D0>.<D1>.<D2>.<D3> <port>\r\n"
 " wiz load default\r\n"
 "[WIZ Test mode]\r\n"
 " wiz loop open\r\n"
 " wiz loop close\r\n"
 "\r\n",
 
 //处理函数
 .pxCmdHook  = &Shell_WIZ_Service,        //见实体函数
 
 //附带数据
 .ucExpParam = 0,
 
  #ifdef SHELL_USE_YMODEM  
    //存储介质
    .phStorage  = NULL,
  #endif
};
/*---------------------* 
*       CLI 链表节
*----------------------*/
Cmd_List_t  WizList   = { &CLI_WizMsg,   NULL,}; //Shell指令的头
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

如配置IP地址输入“wiz wr ip 192.168.1.250\r\n”则可以了。

#环境准备

 硬件开发环境

  • STM32F103系列开发板一块, 带USART1接口。
  • USB转串口线一根。

 软件开发环境

  • MDK4.72或以上
  • SecureCRT串口超级终端

 软件配置

在xc_shell使用“/r/n”作为命令的结束符, 而SecureCRT按下Enter不是输入“/r/n”故而需要按下图设置:此外在《终端》/仿真/高级中选取【本地回显】 

嵌入式分享~IO相关14_单片机_13

#代码介绍

 目录结构

XC_SHELL
├──┬── BSP_LIB    BSP库,硬件相关驱动代买
│  ├──── bsp_ledx.c   基础LED驱动      
│  └──── bsp_tty0.c   调试串口驱动
│
├──┬──MDK-ARM     工程文件
│  └──── Project.uvproj
│
├──┬──SHELL_CFG   SHELL配置头文件
│  └──── user_eval.h
│
├──┬──SHELL_CORE  SHELL内核文件
│  ├──── xc_shell.c   SHELL内核文件
│  ├──── xc_ymodem.c  Ymodem传输协议(默认关闭,在xc_shell.h中启动)
│  ├──── xc_iap.c     Flash的IAP操作,需要bsp_flash.c驱动支持
│  └──── shell_iap.c  shell的用户脚本模板
│
├──┬──SHELL_INC   SHELL头文件
│  ├──── bsp_type.h   驱动结构定义
│  ├──── xc_shell.h   SHELL头文件
│  └──── xconfig.h    硬件无关配置文件
│
├──┬──STM32F10x_StdPeriph_Lib_V3.5.0  STM32的标准外设库
│  └──── ......
│
└──┬──USER        用户文件    
   ├─ .....       
   └──── main.c        main文件
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

工程设置要点

1) 设置使用微库:

嵌入式分享~IO相关14_输出模式_14

2)配置包含和路径,注意使用了“--c99”标准,如图

嵌入式分享~IO相关14_嵌入式硬件_15

3) 编译工程,无错误警告后烧写程序到开发板运行。

最终效果

按图输入一下指令,SHELL平台会回复相关信息。其中输入“led set 0=1”会将信号1分配到物理LED0上;输入“led set 0=2”会将信号2分配到物理LED0上。这样用户编写程序代码时相当于拥有了超级多的LED信号可用,调试时序非常有用。 

嵌入式分享~IO相关14_单片机_16

#添加自己的指令脚本

 源代码示例

假设我要编写一个自己的指令脚本, 来读取MCU的关键信息,关键字为mcu, 文件命名为shell_mcu.c;当输入“mcu rd 0”时显示MCU的FLASH大小,输入“mcu rd 1”时读取MCU的唯一ID信息。shell_mcu.c源代码:

/*********************************Copyright (c)*********************************
**                               
**                                 FIVE工作组
**
**---------------------------------File Info------------------------------------
** File Name:               shell_mcu.c
** Last modified Date:      2017/9/17 15:13:57
** Last Version:            V1.0
** Description:             shell测试
**
**------------------------------------------------------------------------------
** Created By:              wanxuncpx
** Created date:            2017/9/17 15:14:08
** Version:                 V1.0
** Descriptions:            none
**------------------------------------------------------------------------------
** HW_CMU:                  STM32F103
** Libraries:               STM32F10x_StdPeriph_Lib_V3.5.0
** version                  V3.5
*******************************************************************************/
 
/******************************************************************************
更新说明:
******************************************************************************/
 
/******************************************************************************
*********************************  编 译 控 制 ********************************
******************************************************************************/
#define MCU_SHELL               //注释掉时屏蔽iap shell功能
 
#include "xc_shell.h"       //Shell支持文件,含bool,uint8_t..以及串口数据收发操作
/******************************************************************************
********************************* 文件引用部分 ********************************
******************************************************************************/
/*---------------------* 
*     模块驱动引用
*----------------------*/
//#include "net_w5500.h"
 
#ifdef MCU_SHELL
/******************************************************************************
********************************** Shell实例 **********************************
******************************************************************************/
/*---------------------* 
*      CLI指令服务
*----------------------*/
extern bool Shell_MCU_Service(void * pcBuff, uint16_t len );
 
/*---------------------* 
*       CLI 结构
*----------------------*/
const Cmd_Typedef_t CLI_McuMsg=
{
    //识别关键字
    "mcu",
    
    //帮助内容
 "[mcu contorls]\r\n"
 " mcu rd <d>\t\t- Read FLASH information.\r\n"
 "\r\n",
 
 //处理函数
 &Shell_MCU_Service,
 
 //附带数据
 0,
  #ifdef SHELL_USE_YMODEM  
    //存储介质
    NULL,
  #endif
};
 
/*---------------------* 
*     CLI链表节(输出)
*----------------------*/
Cmd_List_t  McuList  = {&CLI_McuMsg      ,NULL}; //IAP指令链表
 
/******************************************************************************
********************************* 函 数 声 明 *********************************
******************************************************************************/
/******************************************************************************
/ 函数功能:STM32F103控制函数
/ 修改日期:2015/7/14 20:22:02
/ 输入参数:none
/ 输出参数:none
/ 使用说明:需要执行约10s
******************************************************************************/
static bool FLASH_ioctl(uint8_t cmd,void * param)
{
    #define UID_ADDR            0x1FFFF7E0  //闪存容量寄存器,值对应KB单位
    #define MAC_ADDR            0x1FFFF7E8  //MCU的唯一ID号,共12个字节
    #define UID_SIZE            2           //UID的字节数
    #define MAC_SIZE            12          //MAC的字节数
 
    //step1: 检查参数
    if(!param)return false;
        
    //step2: 处理数据
    switch(cmd){
      case 0 : {       //获取FLASH的的UID
        uint16_t * ptDst = (uint16_t *)((uint32_t)param+1);
        
        *ptDst = *(uint16_t *)UID_ADDR;
        *(uint8_t  *)param =  UID_SIZE;
        return true;
      }
      case 1 : {       //获取芯片的MAC地址
        uint32_t * ptDst = (uint32_t *)((uint32_t)param+1);
        uint32_t * ptSrc = (uint32_t *)MAC_ADDR;
    
        *ptDst++ = *ptSrc++;
        *ptDst++ = *ptSrc++;
        *ptDst++ = *ptSrc++;
        *(uint8_t  *)param = MAC_SIZE;
        return true;
      }
      default:return false;
    }
}
 
/******************************************************************************
/ 函数功能:文件系统Shel指令处理
/ 修改日期:2013/9/10 19:04:15
/ 输入参数:输入当前的程序版本
/ 输出参数:none
/ 使用说明:none
******************************************************************************/
bool Shell_MCU_Service(void * pcBuff, uint16_t len )
{
    uint8_t    *ptRxd;          //用于接收指令处理
    int         i;
    uint16_t    retval;
    uint8_t     buff[32];
    
    //处理指令
    //--------------------------------------------------------------------------
    ptRxd = (uint8_t *)pcBuff;
    
    if(StrComp(ptRxd,"rd ")) //读取FLASH信息
    {
        int wval;
        
        if(1 != sscanf((void *)ptRxd,"%*s%d",&wval) )return false;
        if( wval>2 )return false;
        if(0==wval) {
            FLASH_ioctl(0,buff);
            retval = *(uint16_t *)(buff+1) ;
            printf("->Flash:\t%dKB\r\n",retval);
            return true;
        }
        else if(1==wval) {
            FLASH_ioctl(1,buff);
            printf("->MAC:\t ");
            for(i=0; i<buff[0]-1; i++){printf("%02X-",buff[i+1]);}
            printf("%02X\r\n",buff[i+1]);
            return true;
        }
        else return false;
    }
    else if(StrComp(ptRxd,"help\r\n"))      //指令帮助
    {
        shell_SendStr((void *)CLI_McuMsg.pcHelpStr);
        return true;
    }
    else return false;
}
 
/******************************************************************************
***********************************   END  ************************************
******************************************************************************/
#endif
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.

 实现步骤

1) 将该文件添加到工程下。

2) 在main.c中用extern 引用McuList,源代码为:

/*---------------------* 
*     Shell指令链表
*----------------------*/
extern Cmd_List_t  McuList;
  • 1.
  • 2.
  • 3.
  • 4.

3)在main.c初始化时添加:

//----------------------------------------------------------
//step1: shell初始化
shell_Init(115200,ledx_cfg);        //初始化shell接口
CLI_AddCmd(&McuList);     //添加模块指令到链表
  • 1.
  • 2.
  • 3.
  • 4.

4)编译工程文件。

5)下载到开发板运行即可在终端下看到新支持的CLI指令:

嵌入式分享~IO相关14_单片机_17

四、PNP与NPN两种三极管使用方法

关于NPN和PNP两种型号三极管的使用和连接方法。

    在单片机应用电路中三极管主要的作用就是开关作用。

PNP与NPN两种三极管使用方法

嵌入式分享~IO相关14_单片机_18

 上图中,横向左侧的引脚叫做基极b,有一个箭头的是发射极e,剩下的一个引脚就是集电极 c。 

    首先来说一下NPN型,这种型号的三极管在用于开关状态时,大都是发射极接地,集电极接高电平,基极接控制信号。

    其次对于PNP型的三极管,用于开关状态时,一般都是发射极接高电平,基极接控制信号。三极管导通时,电流从发射极流向集电极。

三极管的开关原理

    三极管有截止、放大、饱和三种工作状态。

    放大状态主要应用于模拟电路中,且用法和计算方法也比较复杂,我们暂时用不到。

    而数字电路主要使用的是三极管的开关特性,只用到了截止与饱和两种状态。

    三极管的用法特点,关键点在于 b 极(基极)和 e 级(发射极)之间的电压情况,对于PNP 而言,e 极电压只要高于 b 级 0.7V 以上,这个三极管 e 级和 c 级之间就可以顺利导通。

    同理,NPN 型三极管的导通条件是 b 极比 e 极电压高 0.7V。

    总之是箭头的始端比末端高 0.7V 就可以导通三极管的 e 极和 c 极。

嵌入式分享~IO相关14_单片机_19

    以上图PNP三极管为例,基极通过一个 10K 的电阻接到了单片机的一个 IO口上,假定是 P1.0,发射极直接接到 5V 的电源上,集电极接了一个 LED 小灯,并且串联了一个 1K 的限流电阻最终接到了电源负极 GND 上。

    如果 P1.0 由我们的程序给一个高电平 1,那么e到 b 不会产生一个 0.7V 的压降,这个时候,发射极和集电极也就不会导通,那么竖着看这个电路在三极管处是断开的,没有电流通过,LED2 小灯也就不会亮。

    如果程序给 P1.0 一个低电平 0,这时 e 极还是 5V,于是 e 和 b 之间产生了压差,三极管 e 和 b 之间也就导通了,三极管 e 和 b 之间大概有 0.7V 的压降,那还有(5-0.7)V 的电压会在电阻 R47 上。这个时候,e 和 c 之间也会导通了,那么 LED 小灯本身有 2V 的压降,三极管本身 e 和 c 之间大概有 0.2V的压降,我们忽略不计。那么在 R41 上就会有大概 3V 的压降,可以计算出来,这条支路的电流大概是 3mA,可以成功点亮 LED。

三极管饱和状态

    最后一个概念,电流控制。前边讲过,三极管有截止,放大,饱和三个状态。我们要让这个三极管处于饱和状态,就是我们所谓的开关特性,必须要满足一个条件。三极管都有一个放大倍数β,要想处于饱和状态,b 极电流就必须大于 e 和 c 之间电流值除以β。这个β,对于常用的三极管大概可以认为是 100。

    那么上边的 R47 的阻值我们必须要来计算一下了。刚才我们算过了,e 和 c 之间的电流是 3mA,那么 b 极电流最小就是 3mA 除以 100 等于30uA,大概有 4.3V 电压会落在基极电阻上,那么基极电阻最大值就是 4.3V/30uA = 143K。电阻值只要比这个值小就可以,当然也不能太小,太小会导致单片机的 IO 口电流过大烧坏三极管或者单片机。

五、分析GPIO的推挽输出和开漏输出

推挽输出:

可以输出高,低电平,连接数字器件。 

嵌入式分享~IO相关14_数据_20

  • 输出 0 时,N-MOS 导通,P-MOS 高阻,输出0。 
  • 输出 1 时,N-MOS 高阻,P-MOS 导通,输出1(不需要外部上拉电路)。

开漏输出:

输出端相当于三极管的集电极。 要得到高电平状态需要上拉电阻才行。适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内)。

嵌入式分享~IO相关14_单片机_21

  • 输出 0 时,N-MOS 导通,P-MOS 不被激活,输出0。 
  • 输出 1 时,N-MOS 高阻, P-MOS 不被激活,输出1(需要外部上拉电路);可以读IO输入电平变化,此模式可以把端口作为双向IO使用。

嵌入式分享~IO相关14_单片机_22

推挽电路: 

上面的三极管是N型三极管,下面的三极管是P型三极管,请留意控制端、输入端和输出端。 

当Vin电压为V+时,上面的N型三极管控制端有电流输入,Q3导通,于是电流从上往下通过,提供电流给负载。

经过上面的N型三极管提供电流给负载(Rload),这就叫「推」。 

当Vin电压为V-时,下面的三极管有电流流出,Q4导通,有电流从上往下流过。

嵌入式分享~IO相关14_嵌入式硬件_23

经过下面的P型三极管提供电流给负载(Rload),这就叫「挽」。 

以上,这就是推挽(push-pull)电路。

那么什么是开漏呢?

要理解开漏,可以先理解开集。 

如图,开集的意思,就是集电极C一端什么都不接,直接作为输出端口。 

如果要用这种电路带一个负载,比如一个LED,必须接一个上拉电阻,就像这样。

嵌入式分享~IO相关14_嵌入式硬件_24

当Vin没有电流,Q5断开时,LED亮。 

当Vin流入电流,Q5导通时,LED灭。

开漏电路,就是把上图中的三极管换成场效应管(MOSFET)。 

N型场效应管各个端口的名称: 

嵌入式分享~IO相关14_输出模式_25

场效应管是电压控制型元器件,只要对栅极施加电压,DS就会导通。

结型场效应管有一个特性就是它的输入阻抗非常大,这意味着:没有电流从控制电路流出,也没有电流进入控制电路。没有电流流入或流出,就不会烧坏控制电路。

而双极型晶体管不同,是电流控制性元器件,如果使用开集电路,可能会烧坏控制电路。

这大概就是我们总是听到开漏电路而很少听到开集电路的原因吧?因为开集电路被淘汰了。

有人说,开漏单刀单置,推挽单刀双置,这样理解也有一定的道理。