目前算是正式准备搞一搞嵌入式了,在工作中也经常会听到:用“串口”打印一下信息或者用"UART"口打印一下信息,其实就是配置单片机(这里以STM32F103ZET6为例)的USART(Universal Synchronous Asynchronous Receiver Transmitter)模块,然后将一个USB转串口线连接到PC上,串口线的RX,TX和地线分别连接到芯片USART模块的TX,RX和地线。这样就可以在单片机和PC之间相互发送文本信息以进行程序的调试。与此同时我们也经常听到RS232,RS485等相关概念,要命的是这些概念似乎和"串口”、"UART"等概念相互关联,傻傻分不清。如果要成为一名牛逼的程序员的话,还是得把它们之间的关系理清。我通过互联网上的相关资料,以自己的理解对相关概念做一个总结。
在理清这些概念之前先将一些通信的基本概念:同步和异步,并行和串行。
我们先来讲一下同步通信和异步通信, 这里参考了维基百科的内容。在电子设备之间传送数据,不管是同步通信还是异步通信,为了数据那个被正确的传送,都必须有一种“同步方法”。所谓“同步方法”就是接收方可以根据该“同步方法”确定什么时候发送方开始或者结束发送数据以及每一个数据单位(例如比特位)的开始和结束的位置,这样接收方才能在正确的时间对发送方的数据进行采样以接收正确的数据,否则接收到的数据就是错误的。一共有两种同步方法,同步信号方法(1)和异步信号方法(2),他们也就分别对应了同步通信和异步通信:
- 同步信号方法会利用一根额外的信号线,其实也就就是时钟信号线,它往往是发送设备提供的时钟信号,发送设备和接收设备在发送设备提供的同一时钟频率下完成同步。实际上所有的并行通信采用同步通信。
- 异步信号方法没有额外的一根信号线用于同步,接收者和发送者各自使用自己的时钟信号,接收者根据与发送者这事先约定的规来确定数据发送的开始与结束以及数据单位的持续时间。例如异步串行通信中,一般接收双方会确定一致的停止位,数据位的个数、波特率的大小以及是否采用奇偶校验位。接收方可以根据这些信息推测出准确的数据采样时间以接收正确的数据。如果是同步通信则不需要这些额外的用于同步的数据位(开始位,结束位,奇偶校验位)。
图1给了一个串行和并行通信的简单框图。假设现在要发送一个BYTE的数据 0 x A F ( 10101111 ) 0xAF(10101111) 0xAF(10101111),低位优先发送。对于串行通信则是一个比特位一个比特位的发送,只需要一个通道。对于串行通信则是8个比特位分别通过8个通道一次发送完成。
串行通信简单的说就是在通信的时候一个比特位接一个比特位的传输,并且只有同一条数据通道用于传输所有的比特位。但是并行通信可以有多条数据通道来传输数据。假如有8个数据通道的话可以一次性传输8个比特位。因此并行通信的速率比串行通信要快,但是因为并行通信使用了8个数据通道因此使用的资源更多,花费更大。串行通信一般适合于远距离通信,并行通信适合于远距离通信。
因为
U
A
R
T
,
串
口
,
T
T
L
,
R
S
232
,
R
S
485
,
R
S
442
UART,串口,TTL,RS232,RS485,RS442
UART,串口,TTL,RS232,RS485,RS442等相关概念都和异步串行通信相关联,因此先重点讲一下异步串行通信的帧格式(其实这里我也不是很确定,但是我觉得是的,因为我看很多文档介绍异步串行通信的帧格式的时候都是我下面将要介绍的)。帧格式的图示如图2所示。
在没有数据通信时,通信线路上为高电平,第一个低电平告诉接收者数据发送就要开始了。接下来的5到9个比特位用来表示具体的数据位,数据位之后就是奇偶校验位,奇偶校验位可有可无。最后就是0.5到2个停止位用来告诉接收方一个字符发送完毕。这些信息还有波特率在收发双方通信之前会相互确定下来以保证正确的通信。
奇偶校验位的属性可以选择奇校验或者偶校验。
- 奇校验:此时奇偶校验位的作用就是保证所有数据位加奇偶校验位的所有比特位中值为1的比特位的个数为奇数。
- 如果数据位中一共有奇数个值为1的比特位,则此时奇偶校验位的值为0。
- 如果数据位中一共有偶数个值为1的比特位,则此时奇偶校验位的值为1。
- 偶校验:此时奇偶校验位的作用就是保证所有数据位加奇偶校验位的所有比特位中值为1的比特位的个数为偶数。
- 如果数据位中一共有奇数个值为1的比特位,则此时奇偶校验位的值为1。
- 如果数据位中一共有偶数个值为1的比特位,则此时奇偶校验位的值为0。
讲完这些,最后说一下我们在单片机开发中配置“串口”经常接触到的一个参数“波特率”。一直以来我自己都没有很清晰的将“波特率”和“比特率”的概念区分开来,一直处于模糊的状态。我相信很多人也是同样的状态。今天我结合互联网上的搜索结果以及自己的理解以求对“波特率”做一个解释。
在讲解“波特率”之前先做一个简单的陈述:“比特率”针对的是数字信号在数字系统的传输,“波特率”针对的是模拟信号的在实际传输线路(比如电话线)上的传输。因为“波特率”涉及到了信号传输和调制与解调的相关概念,我们先对这些概念做一个了解,如果有讲错的地方还望广大网友指正。
在早期的通信系统中,都是模拟信号,比如收音机。具体的通信过程可以简单的描述为原始要发送的信号经过调制然后通过实际的传输线路传送到接收端,接收端通过解调恢复原始信号。传输的实际信号一般是低频信号,不利于长距离传输,为了长距离传输,一般会将我们实际要传输的信号(调制信号)加载到另一个高频的纯净的载波信号上,使得载波信号的某些属性(幅度、频率或相位)随着调制信号的幅度做相应的改变,分别成为幅度调制、频率调制和相位调制。到了接收端然后再通过相反的过程(解调)来恢复原始的信号。图3是一张来至于维基百科的图片,以图示解释了幅度调制(AM)和频率调制(FM)。具体实现的话大伙还是去好好学学通信原理。
前面讲的针对模拟信号的调制属于模拟调制,当电子计算机出现之后,计算机只能处理数字信号。为了数字信号的有效传输,数字信号也可以调制,数字信号的调制包含两步,解调也就是对应的反过程:
- 将由0和1组成的数字序列调制成基带信号
- 将第一步的基带信号频谱搬移到高频载波信号便于在实际传输线路中传播(其实就对应与模拟信号的调制过程)
下面重点讲一下步骤1,这也是理解“波特率”的关键。这里以数字调制方法相移键控,Phase-shift keying (PSK)为例作一个简单的说明。相移键控简单的说就是以几个相位不同的模拟信号来表示特定的二进制数据。最简单的一种是 B P S K BPSK BPSK以两个相位不同的模拟信号分别表示二进制数据0和1。比如对于信号: s n ( t ) = A ∗ c o s ( t + π ∗ ( 1 − n ) ) s_n(t)=A*cos(t+\pi *(1-n)) sn(t)=A∗cos(t+π∗(1−n)),当 n n n分别取0和1时,分别对应相位为 π \pi π和0,频率和幅度相同的信号,可以用这两个信号来分别表示二进制数据0和1。这样的不同相位的信号的个数是有限的,单个一个信号在传输信道中称为一个“码元”。我们以图4为例,图4中在通信信道中发送了两个码元,第一个为 s 1 ( t ) s_1(t) s1(t)表示二进制数据1,第一个为 s 0 ( t ) s_0(t) s0(t)表示二进制数据0,如果每个码元的持续时间为 0.5 s 0.5s 0.5s,则码元速率为2个码元/秒。这里的码元速率就是“波特率”。现在一个码元可以运载1个比特位的信息,因此“波特率”=“比特率”。
以上的
B
P
S
K
BPSK
BPSK调制方法每个码元只能运载一个比特位的信息,但是
Q
P
S
K
QPSK
QPSK调制方法每个码元可以运载两个比特位的信息(00、01、10、11)。该方法以四个相位不同的模拟信号分别表示四种两个二进制数据位的四种组合。
比如对于信号:
s
n
(
t
)
=
A
∗
c
o
s
(
t
+
1
4
π
∗
(
2
n
−
1
)
)
s_n(t)=A*cos(t+\frac{1}{4}\pi *(2n-1))
sn(t)=A∗cos(t+41π∗(2n−1)),当
n
n
n分别取1、2、3、4时,分别对应相位为
π
4
\frac{\pi}{4}
4π、
3
π
4
\frac{3\pi}{4}
43π、
5
π
4
\frac{5\pi}{4}
45π、
7
π
4
\frac{7\pi}{4}
47π,频率和幅度相同的信号,可以用这四个信号来分别表示四种两个二进制数据位的四种组合00、01、10、11。如果假设在通信信道中一秒钟内发送了两个码元,第一个码元运载二进制数据01,第二个码元运载二进制数据10,码元持续时间为0.5秒。此时码元速率还是为2个码元/秒。即“波特率”为2个码元/秒,但是比特率为2*2比特/秒。此时比特率为波特率的两倍。
再来讲一下
U
A
R
T
(
U
n
i
v
e
r
s
a
l
a
s
y
n
c
h
r
o
n
o
u
s
r
e
c
e
i
v
e
r
t
r
a
n
s
m
i
t
t
e
r
)
UART(Universal\ asynchronous\ receiver\ transmitter )
UART(Universal asynchronous receiver transmitter),现在一般只要做过ARM公司CORTEX内核单片机开发的对这个名词应该都比较熟悉,在这类单片机芯片中一般都集成有该模块,只不过现在一般都是
U
S
A
R
T
(
U
n
i
v
e
r
s
a
l
s
y
n
c
h
r
o
n
o
u
s
a
s
y
n
c
h
r
o
n
o
u
s
r
e
c
e
i
v
e
r
t
r
a
n
s
m
i
t
t
e
r
)
USART(Universal \ synchronous \ asynchronous\ receiver\ transmitter )
USART(Universal synchronous asynchronous receiver transmitter),我们经常利用该模块输出单片机内部的程序运行状态的文本信息。
U
S
A
R
T
USART
USART只比
U
A
R
T
UART
UART多了一个
S
(
s
y
n
c
h
r
o
n
o
u
s
)
S(synchronous)
S(synchronous),即该模块也支持同步通信。那
U
S
A
R
T
USART
USART究竟是个什么东东,按字面翻译是“通用同步异步收发器”。那这么说他就是一个设备了,它在物理上实现了异步串行通信以及同步串行通信的物理电路。因为我们说的那些什么帧格式什么的都只是纸上谈兵,最后实际产生遵循帧格式的实际的电压电流信号才能实现真正的通信。当然一般的
U
S
A
R
T
USART
USART功能不仅限于此,它还有很多强大的功能。具体可以参考具体芯片的用户手册。根据网上的资料对于此时的
U
A
R
T
UART
UART模块一般一个码元仅仅运载一个比特位信息,因此此时“波特率”=“比特率”。
经过上面的讲解,已经有了具体的协议(异步串行协议帧)、已经有了具体的实现电路(
U
A
R
T
UART
UART模块)。
T
T
L
,
R
S
232
,
R
S
485
TTL,RS232,RS485
TTL,RS232,RS485这些关注的点是逻辑电平的表示方式。也就是以多大的电压或者电压组合方式表示二进制位1、以多大的电压或者电压组合方式表示二进制位0。不同的电压或者电压组合方式可以获得不同的传输速率以及传输距离。
在下表中我们简单的描述一下三种逻辑电平的表示方式,注意我们一般在做单片机开发的时候没有多余的串口,会接一个USB转串口线,然后将单片机上对应的UART模块的RX和TX引脚连接到USB转串口线上,其实这个USB转串口线的转换输出就是TTL电平,UART模块一般使用的就是TTL电平。 再就是RS485的RX线或者TX线都是两条线绞在一起的,逻辑电平的判断是根据这两根线的电压差,这样做的目的是为了增强抗干扰能力,延长通信距离和增强通信速率,RS485这三点都比RS232要强。
逻辑0表示电平 | 逻辑1表示电平 | |
---|---|---|
TTL | 输入低电平最大0.8V,输出低电平最大0.4V,典型值0.2V。 | 输入高电平最小2V,输出高电平最小2.4V,典型值3.4V。 |
RS232 | +3V~+15V | -3V~-15V |
RS485 | 电压差:-2V~-6V | 电压差:+2V~+6V |
我们常说的串口(COM口)应该是如图5所示经常在台式电脑上见到的9针口。但是现在应该已经很少使用了,这种接口一般用于串行通信,电平电压应该遵循的是 R S 232 RS232 RS232或者 R S 485 RS485 RS485.虽说它有9根针,只是在以前使用的时候会用到大部分的针脚,现在的话可能就接三根线吧(RX,TX,GND)。据说以前还有一种25针的类似接口,但是现在好像很少见了,好像既可以用来串行通信也可以用来并行通信,并行的话据说用来连接打印机,但是由于并行的同步问题比较难处理,现在基本已经被USB口取代。
/---------------------------------------------------分割线---------------------------------------------------------/
接下来我将使用
S
T
M
32
F
103
Z
E
T
6
STM32F103ZET6
STM32F103ZET6的
U
S
A
R
T
USART
USART模块实现与PC上的串口软件进行简单的收发通信。因为我购买的是正点原子的
S
T
M
32
STM32
STM32精英板,所以以下实现都是在该板子上实现的,以下内容也部分参考了开发板配套的参考资料。
首先讲解一下如何利用
S
T
M
32
STM32
STM32的官方固件库和
M
D
K
5
MDK5
MDK5集成开发环境从零开始建立开发工程。
S
T
M
32
F
10
X
X
STM32F10XX
STM32F10XX的最新固件库可以去官网下载,如图6,图7,图8所示。
当然有了固件库和 M D K 5 MDK5 MDK5集成开发环境,我们还需要安装特定芯片的支持包 D F P , D e v i c e F a m i l y P a c k DFP, Device Family Pack DFP,DeviceFamilyPack。其下载和安装过程如图9,图10,图11,图12,图13,图14所示。
接下来再来简单的看一下 S T M 32 F 1 STM32F1 STM32F1系列的固件库的内容。图15是固件库解压后的一级目录。我们主要用到了目录 L i b r a r i e s Libraries Libraries中的文件。目录 S T M 32 F 10 x _ S t d P e r i p h _ D r i v e r STM32F10x\_StdPeriph\_Driver STM32F10x_StdPeriph_Driver中主要存放了芯片的所有外设的驱动的头文件( i n c inc inc目录)和 . c .c .c文件( s r c src src目录)。目录 C o r e S u p p o r t CoreSupport CoreSupport中主要包含了和 C o r t e x M 3 CortexM3 CortexM3内核相关的一些文件,目录 D e v i c e S u p p o r t DeviceSupport DeviceSupport中主要包含了和具体芯片初始化,启动相关的文件。
目录 U t i l i t i e s Utilities Utilities下的子目录 S T M 32 _ E V A L STM32\_EVAL STM32_EVAL包含了与STM32的相关评估板有关的一些文件。这里说一下什么是评估板,我当时也挺懵的。当芯片公司发布一款新的新的芯片的时候,一般都会发布搭载该芯片的电路板,这样做的目的是可以方便用户评估芯片的性能以及熟悉芯片的各种配置操作。 目录 P r o j e c t Project Project下是官方给的关于一些外设以及使用的示例工程以及模版工程。
接下来将在上面的基础上从零开始构建一个模版工程。首先新建 U S A R T T e s t USART\ Test USART Test文件夹并建立四个子文件夹:
- C o r e Core Core文件夹:将 c o r e _ c m 3. h core\_cm3.h core_cm3.h、 c o r e _ c m 3. c core\_cm3.c core_cm3.c和 s t a r t u p _ s t m 32 f 10 x _ h d . s startup\_stm32f10x\_hd.s startup_stm32f10x_hd.s放入该目录, . s .s .s文件为启动文件,至于为什么要将该文件放入而不是其他 . s .s .s文件,我们可以打开图22所示的 s t m 32 f 10 x . h stm32f10x.h stm32f10x.h文件。打开后其中有一段进行了解释,如图23所示。其中涉及 S T M 32 F 103 STM32F103 STM32F103的一共有4处,但是结合图24我们知道只有 H i g h − d e n s i t y d e v i c e s High-density\ devices High−density devices的 f l a s h flash flash大小符合 S T M 32 F 103 Z E T STM32F103ZET STM32F103ZET的规格。
- L i b Lib Lib文件夹:将目录 S T M 32 F 10 x _ S t d P e r i p h _ D r i v e r STM32F10x\_StdPeriph\_Driver STM32F10x_StdPeriph_Driver中下的 i n c inc inc和 s r c src src文件夹及其所有文件放入该目录。
- O b j Obj Obj文件夹:用来存放编译过程中的中间文件以及 h e x hex hex文件。
- U s e r User User文件夹:将图24和图25中红线圈出的一共7个文件放入该目录中
现在终于可以开始新建工程了。具体过程如图26和图27所示。图28直接 C a n c e l Cancel Cancel。图29为最后得到的结果。
以上只是将工程所需的文件复制到了工程目录之下,刚才新建的工程因此没有和任何文件关联起来,接下来会将所需的文件和工程的 T A R G E T TARGET TARGET关联起来。如图30、图31、图32、图33所示。
前面在复制".s"文件的时候,可以发现有许多不同的".s"文件对应不同类型的相互关联的芯片,同理这里的标准库也不是专门给某一个型号的芯片使用的,而是对应了一系列的芯片。我们可以从文件"stm32f10x.h"中看出来(当然我们只是以这个文件为例子,标准库中肯定也有其它这样类似的文件),如图34,图35所示。其实就是通过宏定义来选择不同的设备,如果每次我们人工手动去修改文件中的宏定义来选择特定的设备,这样容易出错,也不方便因此我们直接通过 M D K MDK MDK的预处理宏定义框来定义相关的宏来选定特定的设备,这里我们需要设定两个宏定义 U S E _ S T D P E R I P H _ D R I V E R , S T M 32 F 10 X _ H D USE\_STDPERIPH\_DRIVER,STM32F10X\_HD USE_STDPERIPH_DRIVER,STM32F10X_HD。同时这里我们也设置了工程的头文件目录。如图36所示。
图26和图27新建工程的过程中会在 U s e r User User文件夹下自动生成 O b j e c t s Objects Objects、 L i s t i n g s Listings Listings和 D e b u g C o n f i g DebugConfig DebugConfig三个文件夹(如图37所示),文件夹 O b j e c t s Objects Objects是用来存储编译过程中的一些中间文件,因为我们已经新建了 O b j Obj Obj文件夹来存储中间文件所以我们需要 O b j e c t s Objects Objects文件夹删除,并配置中间文件放到 O b j Obj Obj文件夹,如图38所示。 L i s t i n g s Listings Listings文件夹里面存放的是生成的"Listing"文件,据说对研究编译过程很有帮助。我们也可以通过图39来设置"Listing"文件的存放位置以及生成那些"Listing"文件。 u V i s i o n uVision uVision支持的文件类型可以参考这里。 D e b u g C o n f i g DebugConfig DebugConfig文件夹里面存放的是和调试、追踪、FLASH编程有关的一些默认设置的配置文件,具体可以参考这里。相应的配置位置可以参考图40、图41.
如果 M D K MDK MDK版本和我一样的话别忘了图43中的设置,不然会报错。组后 m a i n . c main.c main.c文件也需要自己改一下。
正点原子的板子配的是 S T − L I N K ST-LINK ST−LINK的下载调试器,但是我身边有一个 U L I N K ULINK ULINK的下载调试器,所以下面的配置就以 U L I N K ULINK ULINK的下载调试器为配置说明,其实 S T − L I N K ST-LINK ST−LINK的下载调试器的配置基本一样。
- 图45是配置使用 U L I N K ULINK ULINK下载调试器, R u n t o m a i n ( ) Run\ to\ main() Run to main()用来设置进入 D e b u g Debug Debug模式后是否直接执行完初始化相关指令直接跳转到 m a i n ( ) main() main()函数的开头,具体可以参考官方说明。
- 图46是配置同时使用下载调试器来烧录
F
L
A
S
H
FLASH
FLASH来下载程序,而不实用其它工具用于程序烧录,具体可以参考官方说明。
-图47就可以看到 U L I N K ULINK ULINK的下载调试器的一些具体配置,只有在 U L I N K ULINK ULINK的下载调试器连接上电脑上才会显示这些,因为正点原子的 S T − L I N K ST-LINK ST−LINK下载调试器使用的 S W SW SW口,所以这里我也配的SW口。
-图48用于配置程序烧录到 F L A S H FLASH FLASH的一些相关配置( E r a s e S e c t o r s Erase\ Sectors Erase Sectors表示仅仅擦除被烧录的程序用到的 F L A S H FLASH FLASH空间, P r o g r a m Program Program勾上后表示使能 F L A S H FLASH FLASH烧录, V e r i f y Verify Verify勾上后表示使能验证 F L A S H FLASH FLASH上烧录后的程序是否与烧录的程序一直,没有错误, R e s e t a n d R u n Reset\ and \ Run Reset and Run勾上后表示程序烧录完成后复位并开始运行, R a m f o r A l g o r i t h m s Ram\ for\ Algorithms Ram for Algorithms设定 F L A S H FLASH FLASH烧录算法程序在 R A M RAM RAM上的运行地址, P r o g r a m m i n g A l g o r i t h m s Programming\ Algorithms Programming Algorithms其实就是擦除或者向 F L A S H FLASH FLASH下载程序的如软件,因为我们之前安装了 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM32F103ZET6的特定 D F P DFP DFP包,在新建工程的时候也选定了相应的芯片,因此这里会自动加载对应的烧录算法,不用我们去管)具体可以参考官方说明。 - 图49用于配置工程编译完成后输出 H E X HEX HEX烧录文件和其它信息,具体可以参考官方说明。。
- 图50的 X t a l Xtal Xtal用于配置外部晶振的频率,这里正点原子精英板的外部晶振频率为8 M H Z MHZ MHZ,具体可以参考官方说明。。
现在工程模版以及配置工作基本都已经完成了,现在就可以真真的开始利用 U S A R T USART USART模块来实现和PC上串口软件的收发通信了。一般在使用一个模块之前最后通过模块的框图对模块的整个工作流程有一个基本的了解,只有这样才能更好的去写驱动代码。图51是从 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM32F103ZET6的参考手册上截取的 U S A R T USART USART模块的功能框图。图中最下面的是整个模块的时钟源,我们这里使用的是 U S A R T 1 USART1 USART1,从图52中从 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM32F103ZET6的整个系统架构框图可以看出,它的时钟源是 P C L K 2 PCLK2 PCLK2.该时钟源经过分频( U S A R T B R R USART_BRR USARTBRR寄存器里的参数乘以16作为分频系数)后,分别向接收功能和发送功能供应时钟。图中最上面的是具体负责收发的,从 R X RX RX引脚进来的一个个比特位首先进入接收移位寄存器,当移位寄存器里面接收的比特位达到8个时, U S A R T S R USART_SR USARTSR寄存器里的 R X N E RXNE RXNE标志位会被置1,同时这8个比特数据会被放到接收数据寄存器中表示此时可以从接收数据寄存器中读取接收到的一个字节的数据送到 C P U CPU CPU或 D M A DMA DMA,读取完之后 U S A R T S R USART_SR USARTSR寄存器里的 R X N E RXNE RXNE标志位会被置0。这样的过程不断重复来完成数据的接收。 C P U CPU CPU或 D M A DMA DMA的一个字节一个字节的数据在 U S A R T S R USART_SR USARTSR寄存器里的 T X E TXE TXE标志位为1的时候不断的放入发送数据寄存器中,然后通过发送移位寄存器一个比特位一个比特位的发送到 T X TX TX引脚发送出去,当所有8个比特位都发送完毕后 U S A R T S R USART_SR USARTSR寄存器里的 T X E TXE TXE标志位会被置1,这时 C P U CPU CPU或 D M A DMA DMA的一个字节的数据又可以放入发送数据寄存器中。这样的过程不断重复来完成数据的发送。硬件流控制,同步模式我们这里没有用到,当然 U S A R T USART USART模块的功能远不止于此,它还有很对强大的功能,但是我们只是简单的使用了 U S A R T USART USART的一补收发功能。
图53和图54来至于
S
T
M
32
F
103
Z
E
T
6
STM32F103ZET6
STM32F103ZET6的
D
A
T
A
S
H
E
E
T
DATASHEET
DATASHEET,其中说明了
U
S
A
R
T
1
USART1
USART1的接收和发送引脚对应于默认复用功能的
P
A
10
PA10
PA10和
P
A
9
PA9
PA9引脚以及相应的接收和发送引脚应该配置为什么模式。下面直接给代码吧。
u
s
a
r
t
.
c
usart.c
usart.c里面的
u
a
r
t
_
i
n
i
t
uart\_init
uart_init函数用于
u
s
a
r
t
1
usart1
usart1的初始化,先使能
u
s
a
r
t
1
usart1
usart1和
g
p
i
o
a
gpioa
gpioa时钟,然后配置
P
A
10
PA10
PA10和
P
A
9
PA9
PA9引脚的模式,再就是
u
s
a
r
t
1
usart1
usart1的参数,最后使能
u
s
a
r
t
1
usart1
usart1。
U
S
A
R
T
_
S
e
n
d
S
t
r
i
n
g
USART\_SendString
USART_SendString函数用来发送指针
s
t
r
str
str指向的以字符
′
\
0
′
'\backslash0'
′\0′结尾的字符串。
U
S
A
R
T
_
R
e
c
e
i
v
e
S
t
r
i
n
g
USART\_ReceiveString
USART_ReceiveString函数用来接收最大长度为
m
a
x
L
e
n
maxLen
maxLen(这里我们设置为200)个字节的字符串并放到数组
U
S
A
R
T
R
X
B
U
F
USART_RX_BUF
USARTRXBUF中,这里我们在
P
C
PC
PC上使用的是正点原子的串口工具软件,发送出去之后它会在字符串的结尾自动加上回车换行(
0
x
0
D
0x0D
0x0D和
0
x
0
A
0x0A
0x0A两个字节)。因此
U
S
A
R
T
_
R
e
c
e
i
v
e
S
t
r
i
n
g
USART\_ReceiveString
USART_ReceiveString函数在接收到字节数据
0
x
0
A
0x0A
0x0A就表示接收完毕了,此时跳出循环并加上c语言格式字符串结束标识字符
′
\
0
′
'\backslash0'
′\0′。
在
m
a
i
n
.
c
main.c
main.c文件中只是简单的实现了收发功能,当通过PC的串口软件向芯片发送字符串
"
o
n
e
"
"one"
"one",芯片接收到之后向C的串口软件发送字符串
"
W
e
r
e
c
e
i
v
e
d
o
n
e
"
"We\ received\ one"
"We received one",其它以此类推。当通过PC的串口软件向芯片发送的字符串不是
"
o
n
e
"
,
"
t
w
o
"
,
"
t
h
r
e
e
"
,
"
f
o
u
r
"
,
"
f
i
v
e
"
"one","two","three","four","five"
"one","two","three","four","five"等字符串时芯片接收到之后向C的串口软件发送字符串
"
S
e
n
d
s
t
r
i
n
g
e
r
r
o
r
!
!
!
!
!
"
"Send\ string\ error!!!!!"
"Send string error!!!!!"。函数
c
o
m
p
a
r
e
S
T
R
compareSTR
compareSTR用来检测两个字符串是否相同。
/*--------usart.h---------*/
#ifndef __USART_H
#define __USART_H
#include "stdio.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x.h"
#include "stm32f10x_usart.h"
#define USART_REC_MAX_LEN 200
extern u8 USART_RX_BUF[USART_REC_MAX_LEN];
int USART_ReceiveString(USART_TypeDef* USARTx,u8 *str,int maxLen);
void USART_SendString(USART_TypeDef* USARTx,u8 *str);
void uart_init(USART_TypeDef* USARTx,u32 bound);
#endif
/*--------usart.c---------*/
#include "usart.h"
u8 USART_RX_BUF[USART_REC_MAX_LEN]={0};
void uart_init(USART_TypeDef* USARTx,u32 bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
/*----USART1 tx pin is PA9--*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*----USART1 rx pin is PA10--*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*----USART1 communication parameter setting and initialization--*/
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
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(USARTx, &USART_InitStructure);
USART_Cmd(USARTx, ENABLE);
}
void USART_SendString(USART_TypeDef* USARTx,u8 *str)
{
unsigned int index=0;
while(*(str+index)!='\0')
{
USART_SendData(USARTx,*(str+index));
while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE)==RESET);
index++;
}
return;
}
int USART_ReceiveString(USART_TypeDef* USARTx,u8 *str,int maxLen)
{
int pos=0;
while(1)
{
if(pos==maxLen)
{
break;
}
while(USART_GetFlagStatus(USARTx, USART_FLAG_RXNE)==RESET);
str[pos]=USART_ReceiveData(USARTx);
if(str[pos]==10)
{
break;
}
pos++;
}
str[pos-1]='\0';
return pos;
}
/*---------------main.c-----------*/
#include "usart.h"
u8 str1[100]="We received one";
u8 str2[100]="We received two";
u8 str3[100]="We received three";
u8 str4[100]="We received four";
u8 str5[100]="We received five";
u8 compareStr1[10]="one";
u8 compareStr2[10]="two";
u8 compareStr3[10]="three";
u8 compareStr4[10]="four";
u8 compareStr5[10]="five";
ErrorStatus compareSTR(u8 *str1,u8 *str2)
{
int pos=0;
while(1)
{
if((*(str1+pos)!=0)&&(*(str2+pos)!=0))
{
if(*(str1+pos)==*(str2+pos))
{
pos++;
}
else
{
return ERROR;
}
}
else
{
if((*(str1+pos)==0)&&(*(str2+pos)==0))
{
return SUCCESS;
}
else
{
return ERROR;
}
}
}
}
int main(void)
{
uart_init(USART1,115200);
USART_SendString(USART1,"USART transmission and reception test start.!!!!!\r\n");
while(1)
{
USART_ReceiveString(USART1,USART_RX_BUF,USART_REC_MAX_LEN);
if(compareSTR(USART_RX_BUF,compareStr1)==SUCCESS)
{
USART_SendString(USART1,str1);
USART_SendString(USART1,"\r\n");
}
else if(compareSTR(USART_RX_BUF,compareStr2)==SUCCESS)
{
USART_SendString(USART1,str2);
USART_SendString(USART1,"\r\n");
}
else if(compareSTR(USART_RX_BUF,compareStr3)==SUCCESS)
{
USART_SendString(USART1,str3);
USART_SendString(USART1,"\r\n");
}
else if(compareSTR(USART_RX_BUF,compareStr4)==SUCCESS)
{
USART_SendString(USART1,str4);
USART_SendString(USART1,"\r\n");
}
else if(compareSTR(USART_RX_BUF,compareStr5)==SUCCESS)
{
USART_SendString(USART1,str5);
USART_SendString(USART1,"\r\n");
}
else
{
USART_SendString(USART1,"Send string error!!!!!\r\n"););
}
}
}
图55和图56分别是正点原子的串口工具的界面和正点原子的精英板的实拍图。
以上都是我们自己写的字符串的接收以及发送程序,但是如果要使用c语言的库函数 p r i n t f printf printf和 s c a n f scanf scanf来进行发送和接收那应该怎么做。使用c语言的库函数 p r i n t f printf printf和 s c a n f scanf scanf来进行收发的另一个好处是可以格式化输出输入变量,我们以上自己写的收发函数就做不到。进行相应的配置使得可以使用c语言的库函数 p r i n t f printf printf和 s c a n f scanf scanf来进行发送和接收的过程叫做重映射( R e t a r g e t i n g Retargeting Retargeting)。那如果不进行重映射的配置过程,但是我们却在 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM32F103ZET6的程序中使用 p r i n t f printf printf和 s c a n f scanf scanf函数那将是什么结果?我刚开始学习C语言的时候是在 W i n d o w s Windows Windows系统上的,那时 p r i n t f printf printf函数的输出是输出到一个黑色的框里, s c a n f scanf scanf函数也是通过这个黑色的框利用键盘向程序输入数据。这种程序叫做控制台程序,这种程序没有用户界面,只是通过如图56所示的那个黑色的框框(也可以叫做文本终端)来进行输入输出。通常情况下 p r i n t f printf printf函数是和标准输出流链接到一起的,标准输出流又导向了文本终端,因此当我们使用 p r i n t f printf printf函数进行输出的时候就输出到了文本终端,当然也可以通过重映射( R e t a r g e t i n g Retargeting Retargeting)操作使得 p r i n t f printf printf函数输出到其它地方,比如说一个文件。通常情况下 s c a n f scanf scanf函数是和标准输入流链接到一起的,标准输入流又导向了键盘,因此当我们使用键盘在文本终端进行输入的时候就输入到了运行 s c a n f scanf scanf函数的控制台程序中,当然也可以通过重映射( R e t a r g e t i n g Retargeting Retargeting)操作使得 s c a n f scanf scanf函数从其它地方获取输入数据,比如说一个文件。标准输入输出流和文本终端的关系见图57(来至于维基百科)。
但是目前我们的芯片程序没有接键盘和屏幕(或者 L C D LCD LCD),就算接上了也要经过一定的配置才能够实现 p r i n t f printf printf函数输出到屏幕(或者 L C D LCD LCD), s c a n f scanf scanf函数接收来至键盘的输入数据。如果既没有配置又没有重映射( R e t a r g e t i n g Retargeting Retargeting)过程就在 S T M 32 F 103 Z E T 6 STM32F103ZET6 STM32F103ZET6的程序中使用 p r i n t f printf printf和 s c a n f scanf scanf函数那将是什么结果?我们如下面代码段所示在 m a i n main main函数中随意加上一句 p r i n t f printf printf语句然后编译并下载程序在后,程序无法正常运行(不是在 D e b u g Debug Debug模式)。
uart_init(USART1,115200);
/*------------------Added-----------------------*/
printf("Send string error!!!!!\r\n");
/*------------------Added-----------------------*/
USART_SendString(USART1,"USART transmission and reception test start.!!!!!\r\n");
在进入调试模式后如图58所示。光标停在汇编指令 B K P T 0 x A B BKPT\quad 0xAB BKPT0xAB处,我这里如果多点几次图58中红圈中的 r u n run run标志也是可以跑起来的,但是上面代码段中的 p r i n t f printf printf函数的输出语句没有输出到串口软件上,单步运行 F 10 F10 F10, F 11 F11 F11的话好像就跑不起来,网上其它人提到直接在这里卡死动不了。从这里可以知道像 p r i n t f printf printf和 s c a n f scanf scanf函数这样的标准库函数是和底层的调试功能函数(也就是半主机功能)链接到一起的,因此当在程序中使用 p r i n t f printf printf函数会自动使能半主机模式。
那什么是半主机模式?这里有介绍。简单的说半主机模式是一种机制,在这种机制下运行在 A R M ARM ARM芯片上的代码可以使用运行了调试器(比如说 K E I L KEIL KEIL)的 P C PC PC机上的输入(比如键盘)和输出(屏幕)设备和运行在 P C PC PC机上的调试器(比如说 K E I L KEIL KEIL)进行通信。他的具体原理如图59所示。当在芯片中的程序调用 p r i n t f printf printf和 s c a n f scanf scanf函数等函数后会产生相应的异常,运行在 P C PC PC机上的调试器(比如说 K E I L KEIL KEIL)会处理这种异常来完成相应的通信。 K e i l U v i s i o n Keil\ Uvision Keil Uvision不支持半主机模式,至于 I A R IAR IAR是否支持以及如果支持怎样去配置以使用半主机模式我这里没有详细去查找资料。半主机模式也只能在 D e n b u g Denbug Denbug模式下进行,因此如果没有进行重定位和一定的配置就在正常程序中调用 p r i n t f printf printf和 s c a n f scanf scanf函数等函数会产生异常使得程序无法正常运行。
那么怎样可以在芯片程序中正常使用
p
r
i
n
t
f
printf
printf和
s
c
a
n
f
scanf
scanf函数等函数进行
U
S
A
R
T
USART
USART模块的输入输出同时可以避免半主机模式的产生。
K
e
i
l
Keil
Keil的官方文档有专门讲解这个问题的,但是比较复杂,我自己没有亲自去实践,有兴趣的朋友可以去看一看。下面介绍在网络上流传比较多的两种方法。
我们从图60中可以看出
p
r
i
n
t
f
printf
printf和
s
c
a
n
f
scanf
scanf函数等函数的调用层次,第一种方法通过重写
f
p
u
t
c
fputc
fputc和
f
g
e
t
c
fgetc
fgetc函数来实现。在没有重写之前
f
p
u
t
c
fputc
fputc和
f
g
e
t
c
fgetc
fgetc函数分别向标准输出流
s
t
d
o
u
t
stdout
stdout输出一个字符和从标准输入流
s
t
d
i
n
stdin
stdin读取一个字符,重写之后
f
p
u
t
c
fputc
fputc和
f
g
e
t
c
fgetc
fgetc函数分别向
U
S
A
R
T
USART
USART模块输出一个字符和从
U
S
A
R
T
USART
USART模块读取一个字符。可以参考一下下面的文档。
- Building an application without the C library
- Redefining low-level library functions to enable direct use of high-level library functions
- Standard I/O Routines
第一种方法的具体需要添加的代码见下面代码片段中 # i f n d e f M i r c o L I B \#ifndef\ MircoLIB #ifndef MircoLIB条件为真的时候的代码段。用于测试的 m a i n main main函数为下面代码段中的 m a i n main main函数。语句 # p r a g m a i m p o r t ( _ _ u s e _ n o _ s e m i h o s t i n g ) \#pragma\ import(\_\_use\_no\_semihosting) #pragma import(__use_no_semihosting)是向编译器说明所有使用半主机模式的函数都不要被包含在程序中了。如果现在只向程序中添加了语句 # p r a g m a i m p o r t ( _ _ u s e _ n o _ s e m i h o s t i n g ) \#pragma\ import(\_\_use\_no\_semihosting) #pragma import(__use_no_semihosting)而没有将下面代码片段中 # i f n d e f M i r c o L I B \#ifndef\ MircoLIB #ifndef MircoLIB条件为真的时候的代码段的其它语句添加进去,你现在只要不再程序中使用 p r i n t f printf printf和 s c a n f scanf scanf等使用半主机模式的函数就绝对不会出现停在汇编指令 B K P T 0 x A B BKPT\quad 0xAB BKPT0xAB处的情况。但是现在如果在程序中调用 p r i n t f printf printf和 s c a n f scanf scanf等使用半主机模式的函数,比如 p r i n t f printf printf和 s c a n f scanf scanf函数,我这边会出现图61所示的报错。大致意思是本已经已经在程序中要求编译器将所有使用半主机模式的函数都不要被包含在程序中,但是却引用了 _ s y s _ e x i t ( ) \_sys\_exit() _sys_exit()、 _ t t y w r c h ( ) \_ttywrch() _ttywrch()、 _ s y s _ o p e n \_sys\_open _sys_open等函数。标准C语言库函数 p r i n t f printf printf和 s c a n f scanf scanf函数是会使用半主机模式的,这里我们虽然告诉编译器将所有使用半主机模式的函数都不要被包含在程序中,但是我们自己还是在程序调用了函数 p r i n t f printf printf和 s c a n f scanf scanf,从这里Direct semihosting C library function dependencies可以知道C语言库函数 p r i n t f printf printf和 s c a n f scanf scanf依赖于 _ s y s _ e x i t ( ) \_sys\_exit() _sys_exit()、 _ t t y w r c h ( ) \_ttywrch() _ttywrch()、 _ s y s _ o p e n \_sys\_open _sys_open等函数。因这是由于调用函数 p r i n t f printf printf和 s c a n f scanf scanf从而调用了 _ s y s _ e x i t ( ) \_sys\_exit() _sys_exit()、 _ t t y w r c h ( ) \_ttywrch() _ttywrch()、 _ s y s _ o p e n \_sys\_open _sys_open等函数。Redefining low-level library functions to enable direct use of high-level library functions这里讲到如果我们要直接使用C语言库函数 p r i n t f printf printf和 s c a n f scanf scanf,我们还是得去自己去实现 f p u t c fputc fputc和 f g e t c fgetc fgetc等底层函数以及文件结构体。因为我们这里是从 U S A R T USART USART模块输入个输出,因此也不会用到标准输入输出流对象,这里的文件结构体基本是个空的,我们也重新定义了标准输入输出流对象。当我们重写 f p u t c fputc fputc和 f g e t c fgetc fgetc函数以及文件结构体并重新定义了标准输入输出流对象之后就剩下图62所示的一条报错了。从这里可以知道 _ s y s _ o p e n \_sys\_open _sys_open函数是与文件输入输出有关的,而我们这里已经重写 f p u t c fputc fputc和 f g e t c fgetc fgetc函数以及文件结构体并重新定义了标准输入输出流对象,因此自然就不会引用函数 _ s y s _ o p e n \_sys\_open _sys_open了。从这里可以知道 _ t t y w r c h ( ) \_ttywrch() _ttywrch()函数的默认实现是使用半主机模式的,它用来向文本终端( C o n s o l e Console Console,也就是运行在PC上的调试软件)输出一个字符,和以上同样的原因,现在也不会引用了。从这里可以知道所有库的退出函数都依赖于 _ s y s _ e x i t ( ) \_sys\_exit() _sys_exit()函数,虽然我们这里已经重写 f p u t c fputc fputc和 f g e t c fgetc fgetc函数以及文件结构体并重新定义了标准输入输出流对象,但是估计还是得引用推出操作吧,但是这里的退出已经和文件无关,所以这里就简单的改写了一下,里面只有一个赋值语句,这是正点原子的代码,我觉得空函数也是可以的。以上都是我个人的推测。之后就全部OK了。
int main(void)
{
int num=0;
uart_init(USART1,115200);
printf("USART transmission and reception test start.\r\n");
while(1)
{
printf("Please input a number.\r\n");
scanf("%d",&num);
printf("Num is %d.\r\n",num);
}
}
#define MircoLIB
#ifndef MircoLIB
#pragma import(__use_no_semihosting)
void _sys_exit(int x)
{
x = x;
}
struct __FILE
{
int handle;
};
FILE __stdout;
FILE __stdin;
int fputc(int ch, FILE *f)
{
USART_SendData(USART1,ch);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
return ch;
}
int fgetc(FILE *f)
{
int ch=0;
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)==RESET);
ch=USART_ReceiveData(USART1);
return ch;
}
#else
int fputc(int ch, FILE *f)
{
USART_SendData(USART1,ch);
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
return ch;
}
int fgetc(FILE *f)
{
int ch=0;
while(USART_GetFlagStatus(USART1, USART_FLAG_RXNE)==RESET);
ch=USART_ReceiveData(USART1);
return ch;
}
#endif
第二种方法的是使用 M i c r o L I B MicroLIB MicroLIB库,首先你需要在 K E I L KEIL KEIL中勾选 M i c r o L I B MicroLIB MicroLIB库选项,如图所示。 M i c r o L I B MicroLIB MicroLIB库是标准C语言库的一个可替代库,它可以说是标准C语言库的一个极精简版,它主要是为了满足占用极少存储空间的深度嵌入式程序。可以参考一下官方文档。如果你勾选了 M i c r o L I B MicroLIB MicroLIB库选项那就是告诉编译器你不使用标准C语言库了。这时候如果你在芯片程序中调用 p r i n t f printf printf和 s c a n f scanf scanf函数,即使你没有进行任何与重定义相关的配置,此时也不会触发半主机模式。你可以进入调试模式下试一试。这时应该不会卡在汇编指令 B K P T 0 x A B BKPT\quad 0xAB BKPT0xAB处,而是直接跳到 m a i n main main函数的第一行。Tailoring the microlib input/output functions提到如果你想要使用 M i c r o L I B MicroLIB MicroLIB库的 p r i n t f printf printf和 s c a n f scanf scanf函数,你还是得自己实现 f p u t c fputc fputc和 f g e t c fgetc fgetc函数。具体需要添加的代码见上面代码片段中 # i f n d e f M i r c o L I B \#ifndef\ MircoLIB #ifndef MircoLIB条件为假的时候的代码段。用于测试的 m a i n main main函数为上面代码段中的 m a i n main main函数。