处理器与外部设备的通信根据数据传输方式可以分为:
根据数据传输方向的能力,又可分为:
根据是否带时钟同步信号又可分为:
UART(Universal Asynchronous Receiver/Transmitter)通用异步收发传输器,UART 作为异步串口通信协议的一种,工作原理是将传输数据的每个字符一位接一位地传输。是在应用程序开发过程中使用频率最高的数据总线。
UART 串口的特点是将数据一位一位地顺序传送,只要 2 根传输线就可以实现双向通信,一根线发送数据的同时用另一根线接收数据。UART 串口通信有几个重要的参数,分别是波特率、起始位、数据位、停止位和奇偶检验位,数据格式如下图所示:
起始位:表示数据传输的开始,电平逻辑为 “0” 。
数据位:可能值有 5、6、7、8、9,表示传输几个 bit 位数据。一般取值为 8,因为一个 ASCII 字符值为 8 位。
奇偶校验位:用于接收方对接收到的数据进行校验,校验 “1” 的位数为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性,使用时不需要此位也可以。
停止位: 表示一帧数据的结束。电平逻辑为 “1”。
波特率:串口通信时的速率,它用单位时间内传输的二进制代码的有效位(bit)数来表示,其单位为每秒比特数 bit/s(bps)。常见的波特率值有 4800、9600、14400、38400、115200等。
应用程序通过 RT-Thread提供的 I/O 设备管理接口来访问串口硬件,相关接口如下所示:
函数 | 描述 |
---|---|
rt_device_find() | 查找设备 |
rt_device_open() | 打开设备 |
rt_device_read() | 读取数据 |
rt_device_write() | 写入数据 |
rt_device_control() | 控制设备 |
rt_device_set_rx_indicate() | 设置接收回调函数 |
rt_device_set_tx_complete() | 设置发送完成回调函数 |
rt_device_close() | 关闭设备 |
应用程序根据串口设备名称获取设备句柄,进而可以操作串口设备,查找设备函数如下所示:
rt_device_t rt_device_find(const char* name);
通过设备句柄,应用程序可以打开和关闭设备,打开设备时,会检测设备是否已经初始化,没有初始化则会默认调用初始化接口初始化设备。通过如下函数打开设备:
rt_err_t rt_device_open(rt_device_t dev, rt_uint16_t oflags);
oflags
参数支持下列取值 (可以采用或的方式支持多种取值):
#define RT_DEVICE_FLAG_STREAM 0x040 /* 流模式 */
/* 接收模式参数 */
#define RT_DEVICE_FLAG_INT_RX 0x100 /* 中断接收模式 */
#define RT_DEVICE_FLAG_DMA_RX 0x200 /* DMA 接收模式 */
/* 发送模式参数 */
#define RT_DEVICE_FLAG_INT_TX 0x400 /* 中断发送模式 */
#define RT_DEVICE_FLAG_DMA_TX 0x800 /* DMA 发送模式 */
串口数据接收和发送数据的模式分为 3 种:中断模式、轮询模式、DMA 模式。在使用的时候,这 3 种模式只能选其一,若串口的打开参数 oflags 没有指定使用中断模式或者 DMA 模式,则默认使用轮询模式。
DMA(Direct Memory Access)即直接存储器访问。 DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过 DMA 控制器为 RAM 与 I/O 设备开辟一条直接传送数据的通路,这就节省了 CPU 的资源来做其他操作。使用 DMA 传输可以连续获取或发送一段信息而不占用中断或延时,在通信频繁或有大段信息要传输时非常有用。
通过控制接口,应用程序可以对串口设备进行配置,如波特率、数据位、校验位、接收缓冲区大小、停止位等参数的修改。控制函数如下所示:
rt_err_t rt_device_control(rt_device_t dev, rt_uint8_t cmd, void* arg);
控制参数结构体 struct serial_configure 原型如下:
struct serial_configure
{
rt_uint32_t baud_rate; /* 波特率 */
rt_uint32_t data_bits :4; /* 数据位 */
rt_uint32_t stop_bits :2; /* 停止位 */
rt_uint32_t parity :2; /* 奇偶校验位 */
rt_uint32_t bit_order :1; /* 高位在前或者低位在前 */
rt_uint32_t invert :1; /* 模式 */
rt_uint32_t bufsz :16; /* 接收数据缓冲区大小,无法动态更改 */
rt_uint32_t reserved :4; /* 保留位 */
};
RT-Thread 提供的配置参数可取值为如下宏定义:
/* 波特率可取值 */
#define BAUD_RATE_2400 2400
#define BAUD_RATE_4800 4800
#define BAUD_RATE_9600 9600
#define BAUD_RATE_19200 19200
#define BAUD_RATE_38400 38400
#define BAUD_RATE_57600 57600
#define BAUD_RATE_115200 115200
#define BAUD_RATE_230400 230400
#define BAUD_RATE_460800 460800
#define BAUD_RATE_921600 921600
#define BAUD_RATE_2000000 2000000
#define BAUD_RATE_3000000 3000000
/* 数据位可取值 */
#define DATA_BITS_5 5
#define DATA_BITS_6 6
#define DATA_BITS_7 7
#define DATA_BITS_8 8
#define DATA_BITS_9 9
/* 停止位可取值 */
#define STOP_BITS_1 0
#define STOP_BITS_2 1
#define STOP_BITS_3 2
#define STOP_BITS_4 3
/* 极性位可取值 */
#define PARITY_NONE 0
#define PARITY_ODD 1
#define PARITY_EVEN 2
/* 高低位顺序可取值 */
#define BIT_ORDER_LSB 0
#define BIT_ORDER_MSB 1
/* 模式可取值 */
#define NRZ_NORMAL 0 /* normal mode */
#define NRZ_INVERTED 1 /* inverted mode */
/* 接收数据缓冲区默认大小 */
#define RT_SERIAL_RB_BUFSZ 64
接收缓冲区:当串口使用中断接收模式打开时,串口驱动框架会根据 RT_SERIAL_RB_BUFSZ
大小开辟一块缓冲区用于保存接收到的数据,底层驱动接收到一个数据,都会在中断服务程序里面将数据放入缓冲区。
RT-Thread 提供的默认串口配置如下:
#define RT_SERIAL_CONFIG_DEFAULT \
{ \
BAUD_RATE_115200, /* 115200 bits/s */ \
DATA_BITS_8, /* 8 databits */ \
STOP_BITS_1, /* 1 stopbit */ \
PARITY_NONE, /* No parity */ \
BIT_ORDER_LSB, /* LSB first sent */ \
NRZ_NORMAL, /* Normal mode */ \
RT_SERIAL_RB_BUFSZ, /* Buffer size */ \
0 \
}
将缓冲区 buffer 中的数据写入到设备 dev 中,写入数据的大小是 size,调用以下函数:
rt_size_t rt_device_write(rt_device_t dev, rt_off_t pos, const void* buffer, rt_size_t size);
回调函数会在底层硬件数据发送完成后 (例如 DMA 传送完成或 FIFO 已经写入完毕产生完成中断时) 调用,函数设置如下:
rt_err_t rt_device_set_tx_complete(rt_device_t dev, rt_err_t (*tx_done)(rt_device_t dev,void *buffer));
调用这个函数时,回调函数由调用者提供,当硬件设备发送完数据时,由设备驱动程序回调这个函数并把发送完成的数据块地址 buffer 作为参数传递给上层应用。上层应用(线程)在收到指示时会根据发送 buffer 的情况,释放 buffer 内存块或将其作为下一个写数据的缓存。
当串口收到数据时,通过如下函数来设置数据接收指示,通知上层应用线程有数据到达 :
rt_err_t rt_device_set_rx_indicate(rt_device_t dev, rt_err_t (*rx_ind)(rt_device_t dev,rt_size_t size));
若串口以中断接收模式打开,当串口接收到一个数据产生中断时,就会调用回调函数,并且会把此时缓冲区的数据大小放在 size 参数里,把串口设备句柄放在 dev 参数里供调用者获取;若串口以 DMA 接收模式打开,当 DMA 完成一批数据的接收后会调用此回调函数。
rt_size_t rt_device_read(rt_device_t dev, rt_off_t pos, void* buffer, rt_size_t size);
当应用程序完成串口操作后,可以关闭串口设备,关闭设备接口和打开设备接口需配对使用。通过如下函数完成:
rt_err_t rt_device_close(rt_device_t dev);
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步通信总线,常用于短距离通讯,主要应用于 EEPROM、FLASH、实时时钟、AD 转换器、还有数字信号处理器和数字信号解码器之间。SPI 一般使用 4 根线通信,如下图所示:
MOSI –主机输出 / 从机输入数据线(SPI Bus Master Output/Slave Input)。
MISO –主机输入 / 从机输出数据线(SPI Bus Master Input/Slave Output)。
SCLK –串行时钟线(Serial Clock),主设备输出时钟信号至从设备。
CS –从设备选择线 (Chip select)。也叫 SS、CSB、CSN、EN 等,主设备输出片选信号至从设备。
SPI 以主从方式工作,通常有一个主设备和一个或多个从设备。通信由主设备发起,主设备通过 CS 选择要通信的从设备,然后通过 SCLK 给从设备提供时钟信号,数据通过 MOSI 输出给从设备,同时通过 MISO 接收从设备发送的数据。
如下图所示芯片有 2 个 SPI 控制器,SPI 控制器对应 SPI 主设备,每个 SPI 控制器可以连接多个 SPI 从设备。挂载在同一个 SPI 控制器上的从设备共享 3 个信号引脚:SCK、MISO、MOSI,但每个从设备的 CS 引脚是独立的。
主设备通过控制 CS 引脚对从设备进行片选,一般为低电平有效。任何时刻,一个 SPI 主设备上只有一个 CS 引脚处于有效状态,与该有效 CS 引脚连接的从设备此时可以与主设备通信。
从设备的时钟由主设备通过 SCLK 提供,MOSI、MISO 则基于此脉冲完成数据传输。SPI 的工作时序模式由 CPOL(Clock Polarity,时钟极性)和 CPHA(Clock Phase,时钟相位)之间的相位关系决定,CPOL 表示时钟信号的初始电平的状态,CPOL 为 0 表示时钟信号初始状态为低电平,为 1 表示时钟信号的初始电平是高电平。CPHA 表示在哪个时钟沿采样数据,CPHA 为 0 表示在首个时钟变化沿采样数据,而 CPHA 为 1 则表示在第二个时钟变化沿采样数据。
QSPI 是 Queued SPI 的简写,是 Motorola 公司推出的 SPI 接口的扩展,比 SPI 应用更加广泛。在 SPI 协议的基础上,Motorola 公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即 QSPI 协议)。使用该接口,用户可以一次性传输包含多达 16 个 8 位或 16 位数据的传输队列。一旦传输启动,直到传输结束,都不需要 CPU 干预,极大的提高了传输效率。与 SPI 相比,QSPI 的最大结构特点是以 80 字节的 RAM 代替了 SPI 的发送和接收数据寄存器。
SPI 驱动会注册 SPI 总线,SPI 设备需要挂载到已经注册好的 SPI 总线上:
rt_err_t rt_spi_bus_attach_device(struct rt_spi_device *device,
const char *name,
const char *bus_name,
void *user_data)
一般 SPI 总线命名原则为 spix, SPI 设备命名原则为 spixy ,如 spi10 表示挂载在 spi1 总线上的 0 号设备。user_data 一般为 SPI 设备的 CS 引脚指针,进行数据传输时 SPI 控制器会操作此引脚进行片选。
若使用 rt-thread/bsp/stm32 目录下的 BSP 则可以使用下面的函数挂载 SPI 设备到总线:
rt_err_t rt_hw_spi_device_attach(const char *bus_name, const char *device_name, GPIO_TypeDef* cs_gpiox, uint16_t cs_gpio_pin);
挂载 SPI 设备到 SPI 总线后需要配置 SPI 设备的传输参数:
rt_err_t rt_spi_configure(struct rt_spi_device *device,
struct rt_spi_configuration *cfg)
此函数会保存 cfg 指向的配置参数到 SPI 设备 device 的控制块里,当传输数据时会使用此配置参数,struct rt_spi_configuration 原型如下:
struct rt_spi_configuration
{
rt_uint8_t mode; /* 模式 */
rt_uint8_t data_width; /* 数据宽度,可取8位、16位、32位 */
rt_uint16_t reserved; /* 保留 */
rt_uint32_t max_hz; /* 最大频率 */
};
模式: 包含 MSB/LSB、主从模式、 时序模式等,可取宏组合如下:
/* 设置数据传输顺序是MSB位在前还是LSB位在前 */
#define RT_SPI_LSB (0<<2) /* bit[2]: 0-LSB */
#define RT_SPI_MSB (1<<2) /* bit[2]: 1-MSB */
/* 设置SPI的主从模式 */
#define RT_SPI_MASTER (0<<3) /* SPI master device */
#define RT_SPI_SLAVE (1<<3) /* SPI slave device */
/* 设置时钟极性和时钟相位 */
#define RT_SPI_MODE_0 (0 | 0) /* CPOL = 0, CPHA = 0 */
#define RT_SPI_MODE_1 (0 | RT_SPI_CPHA) /* CPOL = 0, CPHA = 1 */
#define RT_SPI_MODE_2 (RT_SPI_CPOL | 0) /* CPOL = 1, CPHA = 0 */
#define RT_SPI_MODE_3 (RT_SPI_CPOL | RT_SPI_CPHA) /* CPOL = 1, CPHA = 1 */
#define RT_SPI_CS_HIGH (1<<4) /* Chipselect active high */
#define RT_SPI_NO_CS (1<<5) /* No chipselect */
#define RT_SPI_3WIRE (1<<6) /* SI/SO pin shared */
#define RT_SPI_READY (1<<7) /* Slave pulls low to pause */
数据宽度: 根据 SPI 主设备及 SPI 从设备可发送及接收的数据宽度格式设置为8位、16位或者32位。
最大频率: 设置数据传输的波特率,同样根据 SPI 主设备及 SPI 从设备工作的波特率范围设置。
配置 QSPI 设备的传输参数可使用如下函数:
rt_err_t rt_qspi_configure(struct rt_qspi_device *device, struct rt_qspi_configuration *cfg);
struct rt_qspi_configuration 原型如下:
struct rt_qspi_configuration
{
struct rt_spi_configuration parent; /* 继承自 SPI 设备配置参数 */
rt_uint32_t medium_size; /* 介质大小 */
rt_uint8_t ddr_mode; /* 双倍速率模式 */
rt_uint8_t qspi_dl_width ; /* QSPI 总线位宽,单线模式 1 位、双线模式 2 位,4 线模式 4 位 */
};
在 RT-Thread 中将 SPI 主机虚拟为 SPI 总线设备,应用程序使用 SPI 设备管理接口来访问 SPI 从机器件,主要接口如下所示:
函数 | 描述 |
---|---|
rt_device_find() | 根据 SPI 设备名称查找设备获取设备句柄 |
rt_spi_transfer_message() | 自定义传输数据 |
rt_spi_transfer() | 传输一次数据 |
rt_spi_send() | 发送一次数据 |
rt_spi_recv() | 接受一次数据 |
rt_spi_send_then_send() | 连续两次发送 |
rt_spi_send_then_recv() | 先发送后接 |
QSPI 的数据传输接口如下所示:
函数 | 描述 |
---|---|
rt_qspi_transfer_message() | 传输数据 |
rt_qspi_send_then_recv() | 先发送后接收 |
rt_qspi_send() | 发送一次数据 |
I2C(Inter Integrated Circuit,即IIC)总线是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。I2C 总线传输数据时只需两根信号线,一根是双向数据线 SDA(serial data),另一根是双向时钟线 SCL(serial clock)。SPI 总线有两根线分别用于主从设备之间接收数据和发送数据,而 I2C 总线只使用一根线进行数据收发。
I2C 和 SPI 一样以主从方式工作,不同于 SPI 一主多从的结构,它允许同时有多个主设备存在,每个连接到总线上的器件都有唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址,同一时刻只允许有一个主设备。如下图所示:
下图所示为 I2C 总线主要的数据传输格式:
当总线空闲时,SDA 和 SCL 都处于高电平状态,当主机要和某个从机通讯时,会先发送一个开始条件,然后发送从机地址和读写控制位,接下来传输数据(主机发送或者接收数据),数据传输结束时主机会发送停止条件。传输的每个字节为8位,高位在前,低位在后。数据传输过程中的不同名词详解如下所示:
开始条件: SCL 为高电平时,主机将 SDA 拉低,表示数据传输即将开始。
从机地址: 主机发送的第一个字节为从机地址,高 7 位为地址,最低位为 R/W 读写控制位,1 表示读操作,0 表示写操作。
应答信号: 每传输完成一个字节的数据,接收方就需要回复一个 ACK(acknowledge)。写数据时由从机发送 ACK,读数据时由主机发送 ACK。当主机读到最后一个字节数据时,可发送 NACK(Not acknowledge)然后跟停止条件。
数据: 从机地址发送完后可能会发送一些指令,依从机而定,然后开始传输数据,由主机或者从机发送,每个数据为 8 位,数据的字节数没有限制。
重复开始条件: 在一次通信过程中,主机可能需要和不同的从机传输数据或者需要切换读写操作时,主机可以再发送一个开始条件。
停止条件: 在 SDA 为低电平时,主机将 SCL 拉高并保持高电平,然后在将 SDA 拉高,表示传输结束。
RT-Thread 将 I2C 主机虚拟为 I2C总线设备,I2C 从机通过 I2C 设备接口和 I2C 总线通讯,相关接口如下所示:
函数 | 描述 |
---|---|
rt_device_find() | 根据 I2C 总线设备名称查找设备获取设备句柄 |
rt_i2c_transfer() | 传输数据 |
查找I2C设备的函数如下所示:
rt_device_t rt_device_find(const char* name);
获取到I2C总线设备句柄就可以使用rt_i2c_transfer()
进行数据传输,函数原型如下所示:
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
I2C 消息数据结构原型如下:
struct rt_i2c_msg
{
rt_uint16_t addr; /* 从机地址,支持 7 位和 10 位二进制地址,需查看不同设备的数据手册 */
rt_uint16_t flags; /* 读、写标志等 */
rt_uint16_t len; /* 读写数据字节数 */
rt_uint8_t *buf; /* 读写数据缓冲区指针 */
}
标志 flags 可取值为以下宏定义,根据需要可以与其他宏使用位运算 “|” 组合起来使用:
#define RT_I2C_WR 0x0000 /* 写标志 */
#define RT_I2C_RD (1u << 0) /* 读标志 */
#define RT_I2C_ADDR_10BIT (1u << 2) /* 10 位地址模式 */
#define RT_I2C_NO_START (1u << 4) /* 无开始条件 */
#define RT_I2C_IGNORE_NACK (1u << 5) /* 忽视 NACK */
#define RT_I2C_NO_READ_ACK (1u << 6) /* 读的时候不发送 ACK */