本文翻译自:http://www.playembedded.org/blog/stm32-usart-chibios-serial/
将STM32 USART与ChibiOS串行驱动程序配合使用
发表于 2018年7月6日 更新了 2018年8月9日
通用同步/异步接收发送器
异步模式下的串行通信是在微控制器和其他设备之间交换数据的最简单和最常用的方法之一。这种通信可以通过 通用同步/异步接收 发送器 (或USART)以及 实际上是USART子集的UART外设来实现。每个STM32微控制器都配有这些外设的多个实例(从2到8),具体取决于微控制器型号。
在本文中,我们将概述串行通信协议和外设,重点关注UART以及如何将其与ChibiOS / HAL的串行驱动程序一起使用。此驱动程序提供了一种简单的方法来使用USART提供缓冲机制作为I / O队列和打印格式化字符串的功能。
并行通信
8位并行通信的一个例子
自计算机科学一开始,计算机之间的数据交换一直是需要的。一开始所有的沟通都是 平行的。
在并行通信中,每个比特是专用线路上的发射机:这意味着这种通信需要每个比特的线路加上同步线路来执行该操作。当然,我们打算发射器和接收器共享相同的参考地。
让我们考虑一个8位并行通信,其中数据在正边沿上被采样。在这种通信中,发送器适当地改变数据线(D0到D7)的状态并切换同步线(TRG)。在TRG的上升沿,接收器采样数据线并完成操作。
并行通信是可靠且快速的,但是非常不实用,因为执行传输所需的导线数量几乎与字大小成比例。
串口通讯
如今,几乎所有的通信都是 串行的。在串行通信中,比特通过单个线路顺序传输,该线路通常称为BUS。这个想法本身很简单,但这种通信需要通信方之间更复杂的同步。
从同步的角度来看,串行协议可以分为两种类型:
- 同步串行,而时钟信号由端点的一个接口产生,并通过特定的时钟线提供给其他接口。生成时钟的通信方被命名为Master而其他Slaves。这种通信的示例是SPI,I2C或USB。
- 异步串行,虽然没有共同的时钟信号,但同步是通过数据线发送附加位(如启动和停止条件),并且所有各方都知道波特率。这种通信的示例是异步RS-232,其可以通过UART实现。
异步通信的一个例子
从通信的角度来看,协议可以分为三种类型:
- 单工通信,而通信是图2中的一种方式。
- 半双工通信,而通信在单线上是双向的。在这种情况下,不可能在同一时刻发送和接收数据。这种通信的示例是 I2C 以及三线SPI。
- 全双工通信,而通信在两条独立的线路上是双向的。在这种情况下,可以在同一时刻在两个方向上交换数据。这种通信的示例是 SPI和UART。
即使同步总线与异步总线相比可以达到更高的波特率,它们也需要额外的时钟线,整个通信与该信号的可靠性严格相关。时钟线(特别是高频)可能受到线路负载效应的干扰影响,线路负载效应也取决于总线长度。
像RS-232这样的异步协议仍然被广泛使用,因为它们非常简单,并且因为它们实际上只需要几根导线(RX和TX)以及参考地,以使两个端点在相对长距离上进行通信。
RS-232
在本文中,我们将给出一个特别强调的串行通信,尤其涉及TH È 推荐标准232 (或RS-232 )。该标准于1960年引入,并正式定义了 数据终端设备(或DTE )与 数据通信设备(DCE )之间的连接。这种标准已经使用很长时间来使用计算机串行端口连接PC外围设备(如调制解调器,打印机和鼠标)。它已逐渐被更多功能的通用串行总线和TCP / IP所取代 标准,但它仍然主要用于通常通过USART外设实现的嵌入式系统。
通讯框架
最初的DTE是机电电传打字机,而DCE通常是调制解调器。该标准基于传输字符的想法,这解释了为什么每个RS-232传输操作的数据大小为8位。
比特编码为 双极非归零级, 也称为 双极NRZL。在这个二进制代码中,“ 1 ”由高逻辑电平(VDD)表示,“ 0 ”由低逻辑电平(-VDD)表示。在NRZ中,空闲状态通常与高逻辑电平相关,逻辑电平为双极性(+/- VDD,VDD为3V至25V),这意味着信号在新的位传输之前不会返回到零。实际VDD值取决于所需的干扰抗扰度。在PC COM端口的情况下,VDD通常为9或5V,但电路能够在高达25V的电压下工作,因为通常在工业等嘈杂环境中采用此电压。
RS-232协议可以通过USART外设实现,但微控制器无法管理双极性信号。相反,STM32'USART产生的信号(通常来自微控制器)产生的信号被编码为 单极性NRZL:“ 1 ”由正电压(VDD)表示,而“ 0 ”由参考接地电压(GND)表示。 这就是为什么像ST3232或MAX3232 这样的逻辑电平转换器需要通过USART将微控制器连接到PC COM端口。
Eclipse终端的截图
由于通信是异步的,数据线必须实现同步机制:每个数据帧以S tart位开始,实际上是从高逻辑状态到低逻辑状态的转换。这允许发送器和接收器彼此同步(假设已知波特率和数据帧格式)。起始位的长度是完全已知的,并且等于以给定波特率发送一位所需的时间。每个数据帧以一个停止位结束,该位是从低到高的转换。停止位的持续时间可配置为0.5,1,1.5或2位时间:这是因为停止位实际上可以作为空闲时间,以允许整个系统保持同步。
如上所述,可以实现同步,因为接收器和发送器都知道波特率。RS-232波特率基于机电电传打印机的倍数。通常支持的比特率包括75,110,300,1200,2400,4800,9600,19200,38400,57600和115200比特/秒。一个有趣的说明是,一些频率为1.843200 MHz的特定晶体振荡器已经长期专门用于此目的。
为了减少与噪声干扰或时钟容差相关的问题,有可能在有效负载传输中添加一个额外的位,以确保字符串中的1位总数始终为偶数或奇数:这称为 奇偶校验位或 校验位,是最简单的错误检测代码形式。因此,奇偶校验位有两种变体: 偶校验位 和 奇校验位。
总而言之,RS-232还允许使用多达六个额外信号进行流量控制,以实现硬件握手。然而,制造商多年来已经构建了许多在标准上实现非标准变化的设备,例如,使用DTR作为流量控制的打印机。
从RS-232到USB CDC
现在几乎不可能看到PC机箱背面的COM端口:这是因为USB已被证明完全能够取代RS-232。为此,USB 还提供称为通信设备类(或CDC)的特定配置文件。在USB CDC中,使用一些USB端点在USB协议中封装RX和TX流,USB端的操作系统使USB设备显示为通常称为虚拟COM端口的传统RS-232端口。
此类设备类也由许多STM32开发套件上提供的ST-Link V2-1调试器实现。在Windows上连接此调试器,CDC将作为ST-Link虚拟COM端口出现在设备管理器中。从器件侧,ST-Link物理连接到STM32的几个引脚,可以通过GPIO备用功能将其重新路由为UART TX和UART RX。有关此连接的信息在电路板原理图上,因此在电路板用户手册中进行了报告。以STM32 Nucleo-64用户手册为例,您会发现ST-Link通过引脚PA2(Arduino连接器D1)和PA3(Arduino连接器D0)连接到USART2。
正如我们将在ChibiOS的后续文章中看到的,还有一些示例在我们的代码中实现CDC并使用STM32 USB作为虚拟COM端口。如果您遇到无法使用ST-Link且您的设备没有USB外围设备的烦人情况,您可能会考虑使用FTDI芯片,这种芯片很容易在互联网上以分线板的形式找到。
STM32 USART
STM32配备UART和USART:最后一个实际上是第一个的超集,它配有时钟线,能够执行全双工和半双工同步通信。在这种情况下,USART实际上就像SPI一样。无论如何,这里我们将专注于异步通信,我们将像UART一样使用USART。
STM32的UART旨在实现许多串行协议:例如,iit实现了两种不同类型的二进制编码,即 单极性NRZL和 曼彻斯特码。在第一种情况下,'1'由VDD表示,'1'由GND表示,在第二种情况下,数据信号上升沿表示'1'而下降沿表示'0'。
单极NRZL和曼彻斯特编码
曼彻斯特代码旨在避免某种逻辑状态的长期永久性:实际上在这种代码中,与NZRL相比,级别状态可以持续一半或一个位时间。这对于那些像IrDA一样无法保证通信信道可靠性的应用非常有用。
在推荐标准485等串行协议中,架构单主机多个从机有空间:在这种网络中,每个从机作为自己的地址和主机可以发送两种数据:从机地址和数据。区分这两种类型的数据协议接受第9位。因此,我们有
- 地址的第9位为“1”;
- 数据的第9位为“0”。
为了实现这种通信,STM32 UART还允许选择在8位和9位有效载荷长度之间选择的数据大小。
异步通信的问题之一是缺乏时钟精度:由于内部产生定时并且通过起始位发生同步,因此可能出现未对准。波特率越高,这种情况就越严重。为了减轻这种影响,UART外设通常会对数据信号进行过采样。STM32的UART能够执行可配置的8/16位过采样:这意味着每个位采样8/16次,以减少误差并减轻噪声影响。
波特率发生器是小数,它能够从ARM外设总线(或APB)开始产生任何发送和接收波特率,后者为每个STM32的外设提供时钟。
每个STM32的UART外设都带有一些专用I / O,可以使用GPIO备用功能进行重新路由。那些IO是:
- 传输线(UART_TX)
- 接收器线(UART_RX)
- 清除发送(UART_CTS)
- 请求发送(UART_RTS)
可以使用两个额外的I / O来控制两个设备之间的串行数据流,这两个I / O是 清除发送(或CTS)和请求发送 (或RTS):在这种情况下,接收器的RTS连接到发送器CTS; 当接收器降低线路时,发送器发送一个字节。
ChibiOS串行驱动程序
ChibiOS / HA L提供了一种通过称为串行驱动程序(通常缩短为SD)的软件驱动程序来快速简便地使用UART的方法。
串行驱动程序使用I / O队列缓冲输入和输出流,这提供了一个主要优点:用户应用程序不必连续服务 中断请求, 因为驱动程序在数据交换内部执行此操作,在这些缓冲区中存储数据。此机制允许轻松实现生产者 - 消费者模式,而无需应用程序端的任何努力:在此方案中,驱动程序填充缓冲区并且用户应用程序使用数据。如果应用程序以比生产速率更快的速度消耗数据,则这种模式不会丢失任何字节,并且考虑到串行波特率和典型的STM32核心速度,这种情况很容易实现。
另外,ChibiOS / HAL提供另一个处理UART驱动程序的驱动程序:UART驱动程序:此驱动程序向应用程序公开IRQ,因此用户必须正确填写一些驱动程序回调以处理字符接收,传输和最终操作错误。该驱动程序将在稍后提供,我们将仅关注串行驱动程序。
串行驱动程序的每个API都以前缀“sd”开头。函数名称为驼峰式,预处理器常量为大写,变量为小写。
驱动程序启用和外设分配
要使用SD,它应该在我们的项目halconf.h文件中启用,我们应该至少为它分配一个作用于mcuconf.h的外设。我们已经介绍了这个概念,但我们会在这里快速回忆一下(如果你想阅读更多内容,也许你应该看看这里)。
可以在halconf.h文件中启用或禁用每个ChibiOS驱动程序,每个项目都有自己的HAL配置头。在原始演示中,通常禁用所有未使用的驱动程序。如果您已经启动了默认演示,您应该已经注意到我们使用串行驱动程序来打印测试套件结果,而且我们很可能您的项目将是默认版本的副本 - 您很可能会发现SD已经启用。
/** * @brief Enables the SERIAL subsystem. */ #if !defined(HAL_USE_SERIAL) || defined(__DOXYGEN__) #define HAL_USE_SERIAL TRUE #endif |
要使用驱动程序,我们必须将USART / UART外设分配给串行驱动程序。这可以在mcuconf.h上完成。下一个代码已从STM32 Nucleo-64 F401RE原始演示的MCU配置头中复制。在这种情况下,USART2被分配给SD。
/* * SERIAL driver system settings. */ #define STM32_SERIAL_USE_USART1 FALSE #define STM32_SERIAL_USE_USART2 TRUE #define STM32_SERIAL_USE_USART6 FALSE #define STM32_SERIAL_USART1_PRIORITY 12 #define STM32_SERIAL_USART2_PRIORITY 12 #define STM32_SERIAL_USART6_PRIORITY 12 |
将同一外设分配给多个驱动程序时编译错误
请注意,无法将相同的外围设备分配给不同的驱动程序,因为这会在IRQ管理上产生冲突,并且项目无法编译。
例如,无法将USART 2分配给串行驱动程序和UART驱动程序,尝试这样做会导致编译错误。在图4中,问题窗口报告由此类操作导出的编译错误。该错误是由于试图将相同的IRQ线(VectorD8)分配给两个不同的驱动程序。
串行驱动程序对象
ChibiOS / HAL按照面向对象的方法设计 ,每个驱动程序由结构表示。此实现允许相同驱动程序的多个实例,因此SD也是如此。每个可用的USART都有一个串行驱动程序实例,但每个实例仅在为驱动程序分配外设时才可用。
/** @brief USART1 serial driver identifier.*/ #if STM32_SERIAL_USE_USART1 || defined(__DOXYGEN__) SerialDriver SD1; #endif
/** @brief USART2 serial driver identifier.*/ #if STM32_SERIAL_USE_USART2 || defined(__DOXYGEN__) SerialDriver SD2; #endif
/** @brief USART3 serial driver identifier.*/ #if STM32_SERIAL_USE_USART3 || defined(__DOXYGEN__) SerialDriver SD3; #endif
/** @brief UART4 serial driver identifier.*/ #if STM32_SERIAL_USE_UART4 || defined(__DOXYGEN__) SerialDriver SD4; #endif
/** @brief UART5 serial driver identifier.*/ #if STM32_SERIAL_USE_UART5 || defined(__DOXYGEN__) SerialDriver SD5; #endif
/** @brief USART6 serial driver identifier.*/ #if STM32_SERIAL_USE_USART6 || defined(__DOXYGEN__) SerialDriver SD6; #endif
/** @brief UART7 serial driver identifier.*/ #if STM32_SERIAL_USE_UART7 || defined(__DOXYGEN__) SerialDriver SD7; #endif
/** @brief UART8 serial driver identifier.*/ #if STM32_SERIAL_USE_UART8 || defined(__DOXYGEN__) SerialDriver SD8; #endif
/** @brief LPUART1 serial driver identifier.*/ #if STM32_SERIAL_USE_LPUART1 || defined(__DOXYGEN__) SerialDriver LPSD1; #endif |
例如,将STM32 USART1分配给串行驱动程序,SD1对象将变为可用。请注意,驱动程序实现的编号与外设编号一致:USART外设1与SD1,USART外设2至SD2,UART外设3至SD3等相关联。
ChibiOS \ HAL中的每个驱动程序都实现了一个有限状态机,因此它适用于串行驱动程序。驱动程序的当前状态存储在名为state的字段中的对象内(您可以使用结构语法作为示例SD2.state访问它)。从ChibiOS \ HAL文档中获取了以下图像, 并说明了SD的有限状态机
串行驱动程序状态机
串口驱动初始化
比较不同的ChibiOS \ HAL驱动程序,我们会发现一些相似性,这将有助于我们熟悉ChibiOS方法。所有驱动程序都有一个初始化函数,用于串行驱动程序
/** * @brief Serial Driver initialization. * @note This function is implicitly invoked by @p halInit(), there is * no need to explicitly initialize the driver. * * @init */ void sdInit(void) { ... } |
如果在HAL配置文件中启用了驱动程序,则会在HAL初始化时自动调用此函数。HAL初始化发生在我们的halInit调用应用程序的主要部分 。我们已经注意到PAL驱动程序中的类似方法,实际上这种方法在每个ChibiOS / HAL驱动程序中都采用。该sdInit函数初始化对象和变量移动驱动器的状态SD_UNINIT到SD_STOP。
请注意,sdInit仅初始化与串行驱动程序相关的变量和对象。用户不得将初始化与配置混淆。
如果在HAL配置文件中启用了ChibiOS驱动程序,则会在HAL初始化时自动初始化该驱动程序。初始化与变量初始化相关,而不是硬件配置。
配置串口驱动
在使用之前,应正确初始化和配置串行驱动程序。此操作由另一个功能执行:开始。
/** * @brief Configures and starts the driver. * * @param[in] sdp pointer to a @p SerialDriver object * @param[in] config the architecture-dependent serial driver configuration. * If this parameter is set to @p NULL then a default * configuration is used. * * @api */ void sdStart(SerialDriver *sdp, const SerialConfig *config) { ... } |
在开始串行通信之前,用户应用程序应至少调用此函数一次。其目的是配置外设,这涉及我们引入的所有可配置性的设置:波特率,奇偶校验位,停止位长度,编码等。
在ChibiOS / HAL中,除PAL之外的每个驱动器都应在使用之前启动。
所述sdStart函数接收两个参数是一个指向我们要启动(例如&SD1,SD2及或任何是我们要使用USART)和一个指向其表示相关的配置结构的串行驱动器的对象。此结构包含与底层硬件严格相关的所有依赖项。这意味着从STM32系列转移到具有不同底层硬件的另一个系列,我们很可能必须对这些配置结构应用一些更改。
启动驱动程序,我们需要一个指向它的指针和指向其配置结构的指针。此结构包含所有硬件依赖性,如果我们将应用程序移植到不同的微控制器上,则必须进行检查。
启动功能通常启用外设时钟。在某些应用中(特别是那些针对低功耗的应用),在不使用时保持外设时钟是不可取的。因此,我们可以使用另一个功能来停止驱动器并停止外设时钟:停止。
/** * @brief Stops the driver. * @details Any thread waiting on the driver's queues will be awakened with * the message @p MSG_RESET. * * @param[in] sdp pointer to a @p SerialDriver object * * @api */ void sdStop(SerialDriver *sdp) { ... } |
当不需要外围设备时,用户应用程序可以调用此功能。几乎是直观的,停止后我们需要一个新的开始,能够再次使用驱动程序。
当外围设备用于一小部分执行时间时,可以停止驱动程序以降低硬件功耗。停止后,应重新启动驱动程序再次使用。
最常见的范例是在需要时启动驱动程序,并在操作完成后停止驱动程序。
/* Starting Serial Driver 2 with my configuration. */ sdStart(&SD2, &my_sd_configuration);
/* Doing some operation on Serial Driver 2. */
/* Stopping. */ sdStop(&SD2);
...
/* Starting again. */ sdStart(&SD2, &my_sd_configuration);
/* Carrying out other operation. */
/* Stopping. */ sdStop(&SD2);
|
这提供了优势,特别是如果外围设备实际上在整个时间量的执行时间的一小部分中使用。
当我们需要即时更改驱动程序配置时,会使用类似的方法。在这种情况下,我们可以使用不同的配置启动外设多次。请注意,不需要停止而是不鼓励,因为在后续启动操作的情况下会跳过某些操作。
如果我们需要动态更改驱动程序配置,可以使用不同的配置多次启动驱动程序。
配置结构
配置结构由两个独立的部分组成:第一部分在所有类型的硬件中保持不变,第二部分严格依赖于硬件
/** * @brief STM32 Serial Driver configuration structure. * @details An instance of this structure must be passed to @p sdStart() * in order to configure and start a serial driver operations. * @note This structure content is architecture dependent, each driver * implementation defines its own version and the custom static * initializers. */ typedef struct { /** * @brief Bit rate. */ uint32_t speed; /* End of the mandatory fields.*/ /** * @brief Initialization value for the CR1 register. */ uint32_t cr1; /** * @brief Initialization value for the CR2 register. */ uint32_t cr2; /** * @brief Initialization value for the CR3 register. */ uint32_t cr3; } SerialConfig; |
考虑到SerialConfig, 速度 代表独立部分,是表示为无符号整数的波特率。其他三个值表示在启动时用于存储在USART驱动程序的 控制寄存器1,控制寄存器2和控制寄存器3中的值。在当前使用的STM32的参考手册中描述了这些寄存器的每个位的含义。
处理配置有一些提示可以帮助您弄清楚如何正确组合它们:
- 看看testhal文件夹和testex文件夹下的演示:在这些演示中,您可以找到一些可以在应用程序中复制和使用的预组合配置。由于配置取决于硬件,请务必为您当前使用的子系列选择一个演示。
- 那些与硬件相关的字段在参考手册中有描述,用它来发现每个位的含义。
- 请记住,start函数通常会对通过配置传递的寄存器值进行一些内部明显的修改。例如,对于STM32F401RE,UART的CR1的第13位是使能,并且应设置为1以启用外设:您可以理所当然地认为sdStart将此位强制为1。
- 有一些简单的技术可以改变寄存器的某些位,而其他位置保持不变。这种操作采用位掩码的名称和采用的范例来处理寄存器read-modify-write。这是你真正应该拥有的知识。如果不是我强烈建议阅读这篇文章 (即使在以后)。
- 您不必定义寄存器位掩码,因为它们已在CMSIS头文件中定义并可用。您可以在chibios182 \ os \ common \ ext \ ST文件夹下找到这些文件。例如,从文件chibios182 \ os \ common \ ext \ ST \ STM32F4xx \ stm32f401xe.h中提取了以下代码片段, 并表示与USART控制寄存器相关的位掩码
/****************** Bit definition for USART_CR1 register *******************/ #define USART_CR1_SBK_Pos (0U) #define USART_CR1_SBK_Msk (0x1U << USART_CR1_SBK_Pos) #define USART_CR1_SBK USART_CR1_SBK_Msk #define USART_CR1_RWU_Pos (1U) #define USART_CR1_RWU_Msk (0x1U << USART_CR1_RWU_Pos) #define USART_CR1_RWU USART_CR1_RWU_Msk #define USART_CR1_RE_Pos (2U) #define USART_CR1_RE_Msk (0x1U << USART_CR1_RE_Pos) #define USART_CR1_RE USART_CR1_RE_Msk #define USART_CR1_TE_Pos (3U) #define USART_CR1_TE_Msk (0x1U << USART_CR1_TE_Pos) #define USART_CR1_TE USART_CR1_TE_Msk #define USART_CR1_IDLEIE_Pos (4U) #define USART_CR1_IDLEIE_Msk (0x1U << USART_CR1_IDLEIE_Pos) #define USART_CR1_IDLEIE USART_CR1_IDLEIE_Msk #define USART_CR1_RXNEIE_Pos (5U) #define USART_CR1_RXNEIE_Msk (0x1U << USART_CR1_RXNEIE_Pos) #define USART_CR1_RXNEIE USART_CR1_RXNEIE_Msk #define USART_CR1_TCIE_Pos (6U) #define USART_CR1_TCIE_Msk (0x1U << USART_CR1_TCIE_Pos) #define USART_CR1_TCIE USART_CR1_TCIE_Msk #define USART_CR1_TXEIE_Pos (7U) #define USART_CR1_TXEIE_Msk (0x1U << USART_CR1_TXEIE_Pos) #define USART_CR1_TXEIE USART_CR1_TXEIE_Msk #define USART_CR1_PEIE_Pos (8U) #define USART_CR1_PEIE_Msk (0x1U << USART_CR1_PEIE_Pos) #define USART_CR1_PEIE USART_CR1_PEIE_Msk #define USART_CR1_PS_Pos (9U) #define USART_CR1_PS_Msk (0x1U << USART_CR1_PS_Pos) #define USART_CR1_PS USART_CR1_PS_Msk #define USART_CR1_PCE_Pos (10U) #define USART_CR1_PCE_Msk (0x1U << USART_CR1_PCE_Pos) #define USART_CR1_PCE USART_CR1_PCE_Msk #define USART_CR1_WAKE_Pos (11U) #define USART_CR1_WAKE_Msk (0x1U << USART_CR1_WAKE_Pos) #define USART_CR1_WAKE USART_CR1_WAKE_Msk #define USART_CR1_M_Pos (12U) #define USART_CR1_M_Msk (0x1U << USART_CR1_M_Pos) #define USART_CR1_M USART_CR1_M_Msk #define USART_CR1_UE_Pos (13U) #define USART_CR1_UE_Msk (0x1U << USART_CR1_UE_Pos) #define USART_CR1_UE USART_CR1_UE_Msk #define USART_CR1_OVER8_Pos (15U) #define USART_CR1_OVER8_Msk (0x1U << USART_CR1_OVER8_Pos) #define USART_CR1_OVER8 USART_CR1_OVER8_Msk
/****************** Bit definition for USART_CR2 register *******************/ #define USART_CR2_ADD_Pos (0U) #define USART_CR2_ADD_Msk (0xFU << USART_CR2_ADD_Pos) #define USART_CR2_ADD USART_CR2_ADD_Msk #define USART_CR2_LBDL_Pos (5U) #define USART_CR2_LBDL_Msk (0x1U << USART_CR2_LBDL_Pos) #define USART_CR2_LBDL USART_CR2_LBDL_Msk #define USART_CR2_LBDIE_Pos (6U) #define USART_CR2_LBDIE_Msk (0x1U << USART_CR2_LBDIE_Pos) #define USART_CR2_LBDIE USART_CR2_LBDIE_Msk #define USART_CR2_LBCL_Pos (8U) #define USART_CR2_LBCL_Msk (0x1U << USART_CR2_LBCL_Pos) #define USART_CR2_LBCL USART_CR2_LBCL_Msk #define USART_CR2_CPHA_Pos (9U) #define USART_CR2_CPHA_Msk (0x1U << USART_CR2_CPHA_Pos) #define USART_CR2_CPHA USART_CR2_CPHA_Msk #define USART_CR2_CPOL_Pos (10U) #define USART_CR2_CPOL_Msk (0x1U << USART_CR2_CPOL_Pos) #define USART_CR2_CPOL USART_CR2_CPOL_Msk #define USART_CR2_CLKEN_Pos (11U) #define USART_CR2_CLKEN_Msk (0x1U << USART_CR2_CLKEN_Pos) #define USART_CR2_CLKEN USART_CR2_CLKEN_Msk
#define USART_CR2_STOP_Pos (12U) #define USART_CR2_STOP_Msk (0x3U << USART_CR2_STOP_Pos) #define USART_CR2_STOP USART_CR2_STOP_Msk #define USART_CR2_STOP_0 (0x1U << USART_CR2_STOP_Pos) #define USART_CR2_STOP_1 (0x2U << USART_CR2_STOP_Pos)
#define USART_CR2_LINEN_Pos (14U) #define USART_CR2_LINEN_Msk (0x1U << USART_CR2_LINEN_Pos) #define USART_CR2_LINEN USART_CR2_LINEN_Msk
/****************** Bit definition for USART_CR3 register *******************/ #define USART_CR3_EIE_Pos (0U) #define USART_CR3_EIE_Msk (0x1U << USART_CR3_EIE_Pos) #define USART_CR3_EIE USART_CR3_EIE_Msk #define USART_CR3_IREN_Pos (1U) #define USART_CR3_IREN_Msk (0x1U << USART_CR3_IREN_Pos) #define USART_CR3_IREN USART_CR3_IREN_Msk #define USART_CR3_IRLP_Pos (2U) #define USART_CR3_IRLP_Msk (0x1U << USART_CR3_IRLP_Pos) #define USART_CR3_IRLP USART_CR3_IRLP_Msk #define USART_CR3_HDSEL_Pos (3U) #define USART_CR3_HDSEL_Msk (0x1U << USART_CR3_HDSEL_Pos) #define USART_CR3_HDSEL USART_CR3_HDSEL_Msk #define USART_CR3_NACK_Pos (4U) #define USART_CR3_NACK_Msk (0x1U << USART_CR3_NACK_Pos) #define USART_CR3_NACK USART_CR3_NACK_Msk #define USART_CR3_SCEN_Pos (5U) #define USART_CR3_SCEN_Msk (0x1U << USART_CR3_SCEN_Pos) #define USART_CR3_SCEN USART_CR3_SCEN_Msk #define USART_CR3_DMAR_Pos (6U) #define USART_CR3_DMAR_Msk (0x1U << USART_CR3_DMAR_Pos) #define USART_CR3_DMAR USART_CR3_DMAR_Msk #define USART_CR3_DMAT_Pos (7U) #define USART_CR3_DMAT_Msk (0x1U << USART_CR3_DMAT_Pos) #define USART_CR3_DMAT USART_CR3_DMAT_Msk #define USART_CR3_RTSE_Pos (8U) #define USART_CR3_RTSE_Msk (0x1U << USART_CR3_RTSE_Pos) #define USART_CR3_RTSE USART_CR3_RTSE_Msk #define USART_CR3_CTSE_Pos (9U) #define USART_CR3_CTSE_Msk (0x1U << USART_CR3_CTSE_Pos) #define USART_CR3_CTSE USART_CR3_CTSE_Msk #define USART_CR3_CTSIE_Pos (10U) #define USART_CR3_CTSIE_Msk (0x1U << USART_CR3_CTSIE_Pos) #define USART_CR3_CTSIE USART_CR3_CTSIE_Msk #define USART_CR3_ONEBIT_Pos (11U) #define USART_CR3_ONEBIT_Msk (0x1U << USART_CR3_ONEBIT_Pos) #define USART_CR3_ONEBIT USART_CR3_ONEBIT_Msk
|
使用默认配置启动串行驱动程序
在默认演示中,我们已经使用串行驱动程序2来打印测试套件结果。回顾那篇文章,我们可以注意到我们没有配置就启动了驱动程序。
/* * Activates the serial driver 2 using the driver default configuration. */ sdStart(&SD2, NULL); |
这是串行驱动程序的特性,当接收到NULL配置时,它以为STM32定义的默认配置开始
/** @brief Driver default configuration.*/ static const SerialConfig default_config = { SERIAL_DEFAULT_BITRATE, 0, USART_CR2_STOP1_BITS, 0 }; |
这种配置意味着没有特性,1位停止,没有硬件流控制,8位数据大小和波特率等于SERIAL_DEFAULT_BITRATE,它在项目HAL配置文件中定义为38400bps。
/** * @brief Default bit rate. * @details Configuration parameter, this is the baud rate selected for the * default configuration. */ #if !defined(SERIAL_DEFAULT_BITRATE) || defined(__DOXYGEN__) #define SERIAL_DEFAULT_BITRATE 38400 #endif |
这种配置与Eclipse Terminal窗口的默认值匹配。
串行操作
放,读,读和写
与串行驱动程序相关的操作是与字符处理容易关联的操作:
sdPut(sdp, char); char token = sdGet(sdp); sdWrite(sdp, string, size); char* buffer = sdRead(sdp, size); |
在这些函数中,sdp是指向我们正在操作的串行驱动程序的指针。请记住,要使用此功能,应启动驱动程序。获取并放置单个字符的作品,读取字符串上的写入但希望指定读取/写入的缓冲区大小。例如
sdPut(&SD2, 'a'); char token = sdGet(&SD1); sdWrite(&SD6, "Hohoho\r\n", 8); char buffer[30] = sdRead(&SD3, 5); |
IO队列
刚才呈现的I / O函数与I / O队列交互,如下图所示
表示如何在串行驱动程序中使用队列的图表
可以在项目的HAL配置文件中配置队列大小,默认值为16个字节或16个字符。在异步模式下,输入和输出完全独立。
例如,让我们考虑RX侧和TX侧仅仅侧重于RX侧。这种实现的范例是生产者 - 消费者。当用户应用程序消耗清空缓冲区的数据时,STM32 USART RX填充输入队列。如果没有以比生产更快的速率消耗数据,则将填充队列,并且从RX到达的字符将丢失。
通过适当的应用程序设计,数据的消耗速度比生产速度快,并且没有数据丢失。这种情况可以实现增加线程的优先级,该线程消耗数据或减少其循环中的睡眠或降低UART波特率。无论如何,如果这样的条件匹配,当输入队列为空或者元素少于我们想要读取的数量时,线程可能会调用sdGet或sdRead。在这种情况下,调用该函数的线程将无限期挂起,直到来自RX线的新数据填充输入队列。因此,sdPut,sdGet,sdWrite和sdRead等函数被定义为阻塞函数。
甲阻挡功能块,直到操作完成调用线程的执行。这些函数以智能方式执行:如果要完成操作,函数必须等待异步事件,例如从USART外设接收新字符,该函数暂停调用线程等待来自USART的中断。这 意味着,当输入队列为空时,我们尝试使用sdGet 读取字符,线程被挂起并在输入队列变空时将恢复。通过这种方式,RTOS执行其他线程,而这个线程暂停。这些函数也称为同步函数,因为调用线程与操作保持同步
甲无阻塞功能启动操作并返回到继续执行调用线程。这种操作在后台执行,通常在硬件中执行,在操作完成时启动中断。这种IRQ通常由驱动程序API以回调的形式公开。这种功能也称为异步功能。
如果您习惯使用JavaScript,则可以看到非阻塞函数,如异步脚本。到目前为止,串行驱动程序没有实现非阻塞功能,因此我们将在后面的文章中深化这一概念。
使用超时放置,获取,读取和写入
在某些应用中,阻塞功能不合适。让我们考虑一下RX线断开的情况。如果我们尝试做一个sdGet,我们的线程将无限期地卡在那里。为了管理这种情况,可以使用超时的相同功能。
sdPutTimeout(sdp, char, timeout); char token = sdGetTimeout(sdp, timeout); sdWriteTimeout(sdp, string, size, timeout); char* buffer = sdReadTimeout(sdp, size, timeout); |
超时应表示为系统滴答。由于这不是立竿见影的,因此有一些宏将毫秒转换为systemtick
TIME_S2I(secs) TIME_MS2I(msecs) TIME_US2I(usecs) |
例如,如果我们想要读取4个字节,超时50ms,我们应该写
char* buffer = sdReadTimeout(&SD2, 4, TIME_MS2I(50)); |
另请注意,也接受TIME_IMMEDIATE和TIME_INFINITE。在第一种情况下,函数将在第二种情况下立即返回,它将像没有超时的函数一样运行。
GPIO相关配置
要使用串行驱动程序,我们需要重新路由GPIO连接,通过PAL驱动程序将它们分配给UART(如果您不熟悉,请查看GPIO文章)。例如,如果我们决定在STM32 Nucleo-64 F401上使用SD6,我们需要检查哪些引脚可以在USART6上重新路由。在备用功能映射中,我们可以读到PC6和PC7可以使用备用功能8 重新映射为USART6_TX和USART6_RX。
因此,我们可以使用此代码在使用SD功能之前更改GPIO配置。我用来在第一个sdStart之前配置引脚。
/* Configuring PC6 as AF8 assigning it to USART6_TX. */ palSetPadMode(GPIOC, 6, PAL_MODE_ALTERNATE(8)); /* Configuring PC7 as AF8 assigning it to USART6_RX. */ palSetPadMode(GPIOC, 7, PAL_MODE_ALTERNATE(8));
|
或者,可以生成自定义板文件,该文件在halInit()调用上预配置这些引脚。
在STM32 Nucleo-64板上,USART2连接到ST-Link虚拟COM端口。请注意,相关引脚已在板上配置用于此用途。看看STM32 Nucleo F401RE board.h, 我们可以注意到GPIOA设置是:
/* * GPIOA setup: * * PA0 - ARD_A0 ADC1_IN0 (input pullup). * PA1 - ARD_A1 ADC1_IN1 (input pullup). * PA2 - ARD_D1 USART2_TX (alternate 7). * PA3 - ARD_D0 USART2_RX (alternate 7). * PA4 - ARD_A2 ADC1_IN4 (input pullup). * PA5 - LED_GREEN ARD_D13 (output pushpull high). * PA6 - ARD_D12 (input pullup). * PA7 - ARD_D11 (input pullup). * PA8 - ARD_D7 (input pullup). * PA9 - ARD_D8 (input pullup). * PA10 - ARD_D2 (input pullup). * PA11 - OTG_FS_DM (alternate 10). * PA12 - OTG_FS_DP (alternate 10). * PA13 - SWDIO (alternate 0). * PA14 - SWCLK (alternate 0). * PA15 - PIN15 (input pullup). */ |
PA2和PA3是实际连接到ST-Link的引脚,并且配置正确。实际上,回顾之前的文章我们已经在默认演示中使用了serial而没有调用palSetPadMode。
串行驱动程序为BaseSequentialStream
ChibiOS / HAL提供了许多抽象接口:其中一个可能很有趣,因为它允许打印格式化的字符串。我们谈论的接口是BaseSequentialStream。
在面向对象的编程中,接口只是一个抽象的API,它是一种行为描述而不是代码实现。在BaseSequentialStream的情况下,指定的行为是该接口有4个方法:
/** * @brief BaseSequentialStream specific methods. */ #define _base_sequential_stream_methods \ _base_object_methods \ /* Stream write buffer method.*/ \ size_t (*write)(void *instance, const uint8_t *bp, size_t n); \ /* Stream read buffer method.*/ \ size_t (*read)(void *instance, uint8_t *bp, size_t n); \ /* Channel put method, blocking.*/ \ msg_t (*put)(void *instance, uint8_t b); \ /* Channel get method, blocking.*/ \ msg_t (*get)(void *instance); |
换句话说,我们声明实现此接口的驱动程序应提供4种方法(write,read,put和get)。我们并不是在讨论如何实现这些方法,而是在定义相关参数和返回类型。
在ChibiOS中,接口的主要目的是概括驱动程序。以这种方式,可以实现在许多不同驱动器上工作的方法。
串行驱动程序实现BaseSequentialStream接口。正如我们稍后将看到的,Serial Driver Over USB(也称为USB CDC)也实现了相同的接口。模糊地在串行和串行USB驱动器上使用BaseSequentialStream功能是可能的。
使用chprintf打印格式化的字符串
ChibiOS提供了一个名为streams的可选模块,它提供了一些功能,包括API chprintf:这个API是printf的ChibiOS版本,并在BaseSequentialStream上打印格式化字符串,就像输出流上的printf一样。流模块位于chibios182 / os / hal / lib / streams 和,因为它被认为是一个可选模块,默认情况下不包含它。
要使用它,我们必须编辑makefile并添加文件 chibios182 / os / hal / lib / streams / streams.mk的包含。这可以在名为“其他文件(可选)”的部分中进行(请参阅下一个代码框的最后一行)。
# Licensing files. include $(CHIBIOS)/os/license/license.mk # Startup files. include $(CHIBIOS)/os/common/startup/ARMCMx/compilers/GCC/mk/startup_stm32f4xx.mk # HAL-OSAL files (optional). include $(CHIBIOS)/os/hal/hal.mk include $(CHIBIOS)/os/hal/ports/STM32/STM32F4xx/platform.mk include $(CHIBIOS)/os/hal/boards/ST_NUCLEO64_F401RE/board.mk include $(CHIBIOS)/os/hal/osal/rt/osal.mk # RTOS files (optional). include $(CHIBIOS)/os/rt/rt.mk include $(CHIBIOS)/os/common/ports/ARMCMx/compilers/GCC/mk/port_v7m.mk # Other files (optional). include $(CHIBIOS)/test/lib/test.mk include $(CHIBIOS)/test/rt/rt_test.mk include $(CHIBIOS)/test/oslib/oslib_test.mk include $(CHIBIOS)/os/hal/lib/streams/streams.mk |
此时我们可以在主文件中包含chprintf.h并使用它。函数chprintf定义为
/** * @brief System formatted output function. * @details This function implements a minimal @p printf() like functionality * with output on a @p BaseSequentialStream. * The general parameters format is: %[-][width|*][.precision|*][l|L]p. * The following parameter types (p) are supported: * - <b>x</b> hexadecimal integer. * - <b>X</b> hexadecimal long. * - <b>o</b> octal integer. * - <b>O</b> octal long. * - <b>d</b> decimal signed integer. * - <b>D</b> decimal signed long. * - <b>u</b> decimal unsigned integer. * - <b>U</b> decimal unsigned long. * - <b>c</b> character. * - <b>s</b> string. * . * * @param[in] chp pointer to a @p BaseSequentialStream implementing object * @param[in] fmt formatting string * @return The number of bytes that would have been * written to @p chp if no stream error occurs * * @api */ int chprintf(BaseSequentialStream *chp, const char *fmt, ...) |
它与printf完全相同, 区别在于接收的第一个参数是指向BaseSequentialStream的指针。因此,我们可以将指向串行驱动程序的指针作为第一个参数传递给chprintf,并在其上打印格式化的字符串
/* This would print "Hello World 1st test" */ chprintf(&SD2, "Hello World %dst test!\r\n", 1);
/* The previous line will give a warning at compile time resolved using this instead. */ chprintf((BaseSequentialStream*)&SD2, "Hello World %dst test!\r\n", 1);
/* If you have to use many chprintf and do not want to cast every time you can use this trick. */ BaseSequentialStream* bsp = (BaseSequentialStream*)&SD2; chprintf(bsp, "Hello "); chprintf(bsp, "World "); chprintf(bsp, "%dst ", 1); chprintf(bsp, "test\r\n");
|
使用前面的代码,我们将看到字符串“Hello World 1st test”,其中包含回车符和换行符三次。请注意,第一行将在编译时返回警告,因为&SD2实际上是指向SerialDriver的指针。使用显式强制转换很容易解决此警告。
进一步的阅读和实践
我们已经为Serial驱动程序计划了一系列示例和练习。如果您有兴趣在Facebook上关注我们,请更新我们的文章。如果您在前一段时间使用STM32 Nucleo-64 F401RE开发板,我已经制作了一个演示,演示了如何使用ChibiOS Shell而不是串行驱动程序。
RT-STM32F401RE-NUCLEO64-Shell-182.zip
shell基本上是一个依赖于I / O流的文本接口,在这种情况下由串行驱动程序提供。此演示已由另一个关于USB CDC的ChibiOS演示复制(已在上面提到)。
另一个有趣的信息来源是testex下的一些演示,它使用chprintf打印传感器数据。如果您使用STM32F3 Discovery或STM32F4 Discovery或STM32 Nucleo + MEMS x-Nucleo(如IKS01A1或IKS01A2),这些演示可能会非常有趣。