[Bonjour STM32] No.7-demo 4.串口通信

​ 又见面啦这里是泡泡。因为某个鸽子的教程鸽了所以就由我来顶锅啦。学习完前面几篇教程之后,你已经可以自豪地说已经入门了STM32单片机的开发了,而这篇文章将要教给你的便是怎样让你的单片机能够和电脑或者其他设备互相交流啦。

​ 前面的教程里,我们都是在把单片机当做一台独立的小型电脑来使用,可以通过编程来自由控制他的每一个IO口的输出,读取按键的输入,执行预先决定好的指令。但是如果问起电脑最重要的功能是什么,在这个时代,大概每一个人都会回答“上网”。网络把无数台电脑连接在一起,构成了现在信息时代的基础,改变了每个人的生活——概括一下重点,信息时代,最重要的就是“通信”。

​ 那么我们的单片机自然也不能总是一片芯片独立地工作,接收各种传感器的通信,了解外部环境信息,接收来自手机或者电脑的指令,甚至接收互联网上的指令或者把工作的结果通过互联网上传,让用户随时随地都能查看…而如果把能够随时随地连接互联网的单片机拿来控制家中的各种电子产品,就是现在很火的物联网(IOT)啦。

​ 说到通信,就要提起一个概念,协议。最简单的理解,通信协议就是两个人交流所使用的语言,语言相同,两个人才能互相理解对方说话的内容,而进行通信的两台机器使用相同的通信协议,他们之间的通信才能正常进行。通信协议有很多种,比如最常见的,我们电脑上网所使用的TCP/IP、http或者https协议,如果一个单片机实现了这些协议,那么他就也能“上网”,也就是一台物联网设备了。不过,互联网通信的硬件和软件部分都比较复杂,我们这里先讲最简单的一种通信接口,串口通信。

​ 串口,英文叫serial port,或者说COM口,UART接口,RS232接口,TTL接口等等(这些名称的具体含义其实在具体使用时各有些不同,有些用起来也不是很严谨,但是基本上提到这几个词的时候指的都是串口这一种通信方式,所以可以先不用纠结太多),是一个历史实在有点悠久的接口,如果有人接触电脑早一些,可能还会对台式机背后的那个扁扁的有两根固定螺丝的“九针串口”有些印象,当然,估计最大的印象是很容易和显示器的25针VGA接口搞混吧。

tGBOAO.jpg

​ 这个就是最传统的串口和并口,并口就是老式的打印机接口啦,不过这玩意现在实在已经几乎消失殆尽了,所以不用在意。串口全名即为串行接口,相对的,并口当然就是并行接口,而串行和并行的区别就在于信号传输的方式。

​ 我们知道,一根数字信号线,也就是单片机的一个IO口,能够表示一位二进制数据,也就0或者1这两种状态,那么如果我们要传输一个字节的信息,也就是8位二进制数,就有两种办法——一种是在一根信号线上,按一定的顺序,先传输一位数据,对方接收到之后再传输下一位,最后把接收到的8位数据按顺序连在一起组成完整的一个字节的数据,这就是串行传输,也就是数据在一根信号线上按照时间一位一位地连成一串传输给对方。

​ 而另一种就简单粗暴很多,直接使用8根信号线,8位数据在8根信号线上一次同时传输给对方,这就是并行传输,数据并排在一起。

​ 两种方法的优缺点显而易见,使用8根线的并行接口可以在串行接口传输一位数据的时间里直接把8位的一个字节信息传输给对方,理论速度快了8倍以上,而如果使用更多的信号线,还可以达成更高的速度。但并行接口需要消耗的硬件IO口资源和使用的电线数量都比串行接口多了很多倍(用的信号线多也代表着数据线更贵,更容易故障,而且数据线也必然会更粗更重)。所以随着电子技术的逐渐进步,信号线可以使用的信号频率越来越高,传统的并口逐渐被淘汰,而使用串行传输的各种通信接口使用越来越广泛。比如日常挂在嘴边的USB接口(Universal Serial Bus,通用串行总线),高清显示器使用的HDMI接口,连接硬盘的SATA接口,都是使用广义上的串行信号传输方式的通信接口(当然这些玩意使用的通信协议和技术都比这里要讲的串口复杂了无数倍,只是底子上也是串行传输的原理啦)。而并行传输方式则基本只在设备内部的高速总线这种信号传输距离很短、速度要求又非常高的场合被使用了,比如电脑DDR内存和显卡PCIE插槽使用的接口。

​ 咳咳,一不小心说偏题了,这里先不提更先进的各种其他的串行接口,说回最原始的计算机串口。这种接口其实只需要三根线就可以进行通信,也就是数据发送——TX(transmit,发送)和数据接收——RX(receive,接收),以及一根信号公共端GND。虽然很简单,但是需要特别注意的是,使用串口通信的两个设备之间的发送端和接收端是互相交叉的,很容易理解,一方的发射信号自然就是被另一方接受的信号,因此在连接电路的时候,双方的TX和RX必须交叉连接——这一点坑死过无数初学单片机的同学而百思不得其解(比如我)。当然,有个更简单的办法,如果你的串口怎么也没法正常运行,就直接把两根线调换一下再试一试吧。

tG6gFf.jpg

​ 上面说到,串口实在是一种很古老的接口,历史反正比Windows98的电脑或者16M容量的U盘还要早很多,这两样东西你们就算见过肯定也早已经被扔进了历史的垃圾堆,所以这玩意实际的性能也是很对的起他的时代——传输速度很慢,别说跟现在常用的usb typeC相比,就是跟“天翼3G真是太快啦”相比这玩意的通信速度也是慢的可以。

​ 但是串口之所以到了现在还在各种电子产品开发中有着广泛的使用,就是因为简单易用占用资源少,少到什么程度呢,就算是我在前面的文章里拿来做背景板狠狠批判的最原始的8051单片机也自带了串口通信的功能,而即使是早就落后了的usb2.0接口也是到了STM32这里才成为单片机的标配之一。至于它传输速度慢的缺点——单片机也不需要下载什么高清大图或者4k视频,用来让连接的传感器告诉单片机“现在室温32度“这种简短的通信内容,串口的速度已经完全足够了。

​ 说了这么多,下面就是实践环节啦。理所当然的,现在我们用的笔记本电脑上早就不可能找到古老的9针串口接口了,所以我们使用USB转串口芯片进行通信——你手里的大部分STM32开发板上都已经自带了这个芯片,只需要用USB数据线把开发板和电脑连接起来,就可以当做真正的串口一样使用了。当然夏老师为这个教程配套的STM32小板子上也配备了这个功能。

​ 不过因为夏老师的板子还没有做好…所以我这里就只能使用淘宝爆款的STM32F103C8T6核心板(大概十块钱一块)和USB转TTL模块(淘宝几块钱包邮一抓一大把)来进行示范了。如果想尝试的话也可以很简单的买到,然后按照上面的那张图用杜邦线把模块和单片机连接起来就可以了(记得给USB转TTL模块安装好电脑驱动)。

tGobzq.png

​ 这里就是我这次用到的硬件设备啦。红色的是ST-LINK下载器,用来给把写好的程序下载写入到STM32单片机中。

​ 这里我们略过那些关于串口硬件底层协议、信号逻辑时序之类的讲解,因为这些东西已经有ST的工程师帮我们处理好了。如果解释的话又得在这里写至少上千字而且写出来我自己都不想看你们肯定更不想看。HAL库的好处便在于他的高度封装化,把底层的技术装进黑箱子里,让我们可以简单的用几句代码实现功能,而不用关心那些复杂的底层实现。这样我们就可以更专注于实现想要的功能和想法,更快速的完成开发,享受玩电子的乐趣,至于那些基础而又底层的东西自然有比我们更专业的开发工程师来处理。

​ 这就是技术的分工合作,就像一个做应用软件开发的程序员不需要了解电脑CPU的微电子技术细节。当你做的东西越复杂越会体会到这样的方便之处——比如USB的通信协议要是想完整的讲明白大概在这里再放几万字也不一定够用。

​ 如果你想了解一下串口的底层实现的话,可以参考这个:https://www.cnblogs.com/menlsh/archive/2013/01/28/2880580.html

​ 那么我们打开STM32CubeMX,照着之前的例子新建一个工程,设置好时钟、调试接口和晶振。

tGvk6J.png

​ 记得在ProjectManager——Code Generator选项卡下,选上那个Generate peripheral initialization什么什么这个选项,这样会为每个外设创建独立的代码文件,方便独立修改代码,看起来也比较清晰。然后把最上面的选框改成Copy only the necessary library files,只在工程里复制用到的外设的库文件,可以节约一点工程目录占用的空间。

tGxF4f.png

​ 然后我们回到配置选项卡,点左侧的Connectivity,这个选项卡下面都是和通信有关的外设,我们点USART1,也就是STM32内置的串口控制器1,把右边的Mode改成Asynchronous,也就是异步模式,这样就启用了这个串口。

​ 可以在右边的管脚配置里看到TX被配置在PA9引脚上,RX在PA10引脚上。

​ 左下角有一些串口的参数配置,这些我们这里全都保持默认——这几个默认配置就是最常用的串口配置参数。

​ 需要特别解释的,这些参数中最重要的一个便是波特率,可以看到后面的单位是Bits/s,很好理解,这个参数决定了串口传输数据的速度,115200,这是串口常用的波特率里很快的一个速度了,但是计算一下,11.5kbits/s,除以8换算成字节,也就是不到2kBytes/s,每秒传输一千多个字节的数据,就是这样一个速度了——比较老的USB2.0的U盘拷贝文件的速度大概在几个MBytes到十几MBytes每秒之间,对比一下可见串口这玩意确实是一个在速度上十分落后的通信接口了。

​ 至于其他的配置,反正保持默认就可以了,没必要纠结他。

tGz2SU.png

​ 接下来还是在左下角的配置页面,点NVIC选项卡,这里是和这个外设有关的中断设置——中断的概念在上一篇文章里已经讲过了。因为串口接收到数据的时间是不确定的,所以这里很适合使用中断功能,我们把这个中断的Enabled勾选上,启用这个串口的中断。

​ 然后我们生成代码,打开Keil工程。

tJSl1U.png

​ 可以看到左侧的项目文件里面比起之前多出了一个包含串口代码的“USART.c”文件。

​ 我们首先打开这个usart.c文件。

tJpF4x.png

/* USER CODE BEGIN 0 */

#include <stdio.h>

/* USER CODE END 0 */

​ 在第一个用户代码段中包含stdio.h这个标准库文件——这个库文件一定都不会陌生,大一学习C语言时使用频率最高的printf函数就包含在这个标准库中。而在单片机代码里包含这个库文件之后,再将标准输出重定向到串口输出,我们就可以像在电脑上使用printf在屏幕上输出计算结果一样,把自己想要传输的数据直接通过串口传输到电脑或者串口连接的其他设备上,可以说是非常的方便。

​ 在以后调试一些比较复杂的单片机代码时,如果想要实时监测单片机处理的一些数据的值,就可以很简单的用这种方法把那些数据定时printf到电脑上来查看。

/* USER CODE BEGIN 1 */
#ifdef __GNUC__
  /* With GCC/RAISONANCE, small printf (option LD Linker->Libraries->Small printf
     set to 'Yes') calls __io_putchar() */
  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */
/**
  * @brief  Retargets the C library printf function to the USART.
  * @param  None
  * @retval None
  */
PUTCHAR_PROTOTYPE
{
  /* Place your implementation of fputc here */
  /* e.g. write a character to the EVAL_COM1 and Loop until the end of transmission */
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);

  return ch;
}

/* USER CODE END 1 */

tJ9wOe.png

​ 这一段代码就是把printf函数的输出重定向到串口通信中进行传输的代码了,我们把它复制到文件尾部的用户代码段中。

​ 这里调用的函数HAL_UART_Transmit,也就是HAL库的串口发送函数,每当调用printf函数时,就会使用这个函数将数据通过串口传输出去。后面的参数huart1自然就是使用的串口号了,我们只打开了第一个串口,所以这里是uart1。

tJCl1f.png

​ 接下来打开main.c,也就是主程序文件,在开头也包含上这个标准库文件。

  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
        printf("Hello,World!\n");
        HAL_Delay(1000);
  }
  /* USER CODE END 3 */

tJC7HH.png

​ 在while(1)主循环中写入上面的代码——这次是真的Hello,world了。功能很简单,使用串口向电脑发送Hello,world,然后延时一秒,再次发送。只用这一句代码,我们就可以实现单片机向电脑的串口数据传输了。

​ 然后我们点击编译,下载,把代码下载进STM32单片机核心板里。把USB转串口模块和单片机连接好,插在电脑上,然后打开串口助手软件——这个软件的下载链接和代码工程一起放在我们的GitHub里。

tJifOO.png

​ 打开设备管理器,展开端口列表,可以看到USB转串口的串口号。

tJACQ0.png

​ 选择刚才找到的串口号,配置好波特率,然后打开串口,按下核心板上的复位按钮,复位STM32单片机!

​ 可以看到USB转串口模块上的接收指示灯每隔一秒钟闪亮,而串口助手的接收区里,每隔一秒钟便接收到一条由单片机发送来的“Hello,world”信息。

tJKyz4.gif

​ 通信是双向的,做完发送,自然要做接收。单片机接收电脑发来的数据,然后进行判断,也就可以通过电脑控制单片机运行特定的指令了。

​ 这里我们使用中断接收方式,当STM32单片机串口接收到来自电脑的数据,就自动调用串口接收中断处理函数,判断接受到的数据内容,并做出对应的处理。

​ 首先我们回到CubeMX,把IO口PC13打开,也就是核心板上安装了一个LED灯的IO口,用这个LED来指示我们通过电脑控制的外设。

tJAz7D.png

​ 重新生成代码,然后删掉我们刚才在主函数里写的那两行代码。

tJEoKP.png

​ 定义一个全局变量用来存储接收到的电脑端数据。

tJVUqf.png

​ 把这行代码写在main函数的while(1)循环之前——这句代码只会在单片机复位之后被执行一次,然后单片机主程序进入无限的死循环,这个工程中while(1)主循环中没有一句代码,所有的功能都通过中断实现。

HAL_UART_Receive_IT(&huart1,&rx,1);

​ 这句代码通过开启串口的中断模式接收,HAL库中后缀带有IT两字的都代表通过中断实现。参数中的uart1代表使用的串口号,第二个参数是存储接收到的数据的变量地址,第三个参数是接收数据的长度——这里我们只接收一个字符。

​ 当STM32的串口接收到的数据长度到达第三个参数要求的长度后,就会调用串口接收中断处理函数。

​ 我们把这个函数也写在main.c的末尾。

tJZXXq.png

​ 中断处理函数的函数名是由HAL库预先规定好的,每个名字代表着引起中断的不同事件。这个中断函数的名字,RxCpltCallback,RX代表接收,Cplt是complete的缩写,也就是串口接收完成的中断函数。

​ 不同中断事件对应的处理函数名可以在stm32f1xx_it.c这个文件里找到。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if(rx == 'a')
    {
        HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
        printf("OK!\r\n");
    }
    HAL_UART_Receive_IT(&huart1,&rx,1);
}

​ 这个例子里我们规定电脑发来的控制指令是“a”(实际使用的指令一般是一个更长的字符串,这里为了简化只使用了一个字符),进入中断后,先判断接收到的数据是不是小写字母“a”,如果是就翻转IO口,改变LED灯的开关状态,并且用串口向电脑发送“OK”消息,告知电脑单片机成功接收到了指令并执行成功。

​ 然后再次调用串口接收函数,退出中断处理函数,等待下一次数据接收完成,再次进入中断处理函数进行处理。

​ 我们编译下载这个程序,复位单片机,然后打开串口,在下面的发送区里写入字母a,点击手动发送,可以看到LED灯的由亮变灭,同时接受区收到了单片机发回的OK信息,再发送一次,LED灯再次打开,同时又一次发送OK信息。

tJM4ns.gif

​ 这样,我们就实现了用电脑上的指令来控制单片机的行动。如果把LED灯换成一个继电器控制的电灯,那这就是一个简单的电脑控制的程控台灯了。

​ 除了和电脑通信,还有更有意思的玩法,如果你去淘宝上买一个串口接口的蓝牙模块,把它和STM32单片机连接在一起,然后用手机连接蓝牙,就可以实现单片机和手机的蓝牙无线通信了——然后下载一个手机串口助手,把指令用蓝牙发给单片机,就可以用手机蓝牙来无线控制单片机的工作了。这样就可以很简单的做出一个自己的蓝牙无线台灯,或者蓝牙无线风扇,这就是最简单的智能设备啦。

tJmho8.png

这里是项目工程文件和串口助手软件的下载链接:https://github.com/emoestudio/STM32Demo04.git

发表回复

这篇文章有 3 个评论

  1. 第 Autumn页

    如果程序都没问题,那么可能的一个原因是 option for target -> target -> use microLIB 没有勾选。 勾选后,程序成功跑通。调了几天的痛苦教训 : (

    1. 第 Floyd-Fish页

      这个是mdk的老坑了23333,在cubeide的gcc-arm编译器环境中直接重定向就可以了

  2. 第 Luey页

    由于没有基础而辜负了绫依的手把手教学真是对不起