跳转至

5.4 STM32驱动开发-IIC和USART

前言

讲解的这部分内容和代码在工程目录中的 Drivers 目录下的 stm32f10x_driver_iic.cstm32f10x_driver_iic.hstm32f10x_driver_usart.cstm32f10x_driver_usart.h 中。

IIC协议

简介

IIC即 Inter-Integrated Circuit(集成电路总线)是一种简单、双向、二线制同步串行总线,这种总线协议是由飞利浦半导体在八十年代初设计出来的。IIC是一种多向控制总线,也就是说多个芯片可以连接到同一总线结构下,同时每个芯片都可以作为实时数种统一的通信标准,大家都按照这个标准在自家产品中去实现,这样就可以很方便地在各个产品中据传输的控制源。IIC总线协议简单来说就是为了解决各大厂商众多产品之间相互通信问题所提出的一传送数据了。

IIC总线由两根信号线组成,一个是双向数据线SDA,另一根线是时钟线SCL,所有接到IIC总线设备上上的串行数据SDA线都接到总线的SDA上,各个设备的时钟线SCL都接到总线的SCL上。IIC协议在 Breeze Mini 上主要用于读取支持IIC协议的MPU6050和HMC5883L传感器的数据。

IIC操作

1. 总线空闲

SCL高电平,SDA高电平

2. 起始位

SCL高电平,SDA出现下降沿

breeze_embedded_iic_startbit

3. 终止位

SCL高电平,SDA出现上升沿

breeze_embedded_iic_endbit

4. 数据传输

SCL低电平时SDA的高低电平变化,且SDA在SCL为高电平时不变换(因为此时SDA的状态挥别写入从机)

breeze_embedded_iic_databits

IIC的时钟线SCL最高频率:400K

应答:当IIC主机(可以是发送者也可以是接受者)将8位命令或数据传送出去后,会将SDA信号线设置为输入,并等待从机应答(等待SDA由高电平跳变为低电平),若从机应答正确,则表明命令或数据传送成功,否则传送失败。注意应答信号总是由数据接收方发给数据发送方的

IIC器件地址:每个支持IIC的外部器件都有一个IIC总线器件地址,这些器件地址有的在出厂时就已经设定好了,有的是只有部分位已经确定,未确定位可以通过在硬件设计上拉低或高某个管脚的电平来设置。比如EEPROM的前四位地址已经确定为1010,后三个地址则可以设置,这样在一个IIC总线上就可以支持最多挂载8个EEPROM。

5. 传送包结构

以下是IIC总线协议传送的包结构图:

breeze_embedded_iic_package_structure

上图中开始信号之后的7位地址表示器件地址,第8位表示主机读或写位,0为写,1为读,接着是响应位。

6. 字节读写时序

IIC器件单字节写时序为:

breeze_embedded_iic_single_data_write

IIC器件的多字节写时序:

breeze_embedded_iic_multi_data_write

IIC器件单字节读时序(注意最后产生无应答信号):

breeze_embedded_iic_single_data_read

硬件连接

IIC属于 Breeze Mini 主微控制器STM32F103TBU6的通信协议实现,是没有外部电路连接的。

软件设计

虽然STM32F103TBU6也提供IIC总线的外设接口,可以直接通过官方提供的固件库实现IIC协议,但是由于STM32F103系列的IIC外设接口使用起来不是很稳定,所以飞控代码中是直接通过端口高低电平操作和延时函数来模拟实现IIC协议的。整个代码比较好理解,可以看做是用C语言对上述协议"翻译"了一遍,所以读者可以依照上述协议来对照地理解代码。因为这部分代码比较简单,所以只简单介绍一下初始化函数。完整代码在工程目录下的 Drivers 子目录下的 stm32f10x_driver_iic.cstm32f10x_driver_iic.h 中。

IIC端口初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void IIC_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
}

由于是用GPIO端口模拟实现的IIC协议,即 按照高低电平和相应时序操作GPIO端口。所以这部分初始化代码和初始化LED部分(同样也是GPIO端口操作)是一样的,只不过这里初始化的端口是 PB6PB7

下面的代码是所实现的IIC端口操作的全部接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
extern void IIC_Init(void);
extern void IIC_SendAckSignal(void);
extern void IIC_SendStartSignal(void);
extern void IIC_SendStopSignal(void);
extern void IIC_SendNAckSignal(void);
extern void IIC_WaitAckSignal(void);
extern u8   IIC_ReadByte(u8 iic_addr, u8 reg_addr);
extern u8   IIC_ReadBytes(u8 dev_addr, u8 reg_addr, u8 byte_nums, u8 *data);
extern u8   IIC_ReadOneByte(u8 ack);
extern u8   IIC_WriteBit(u8 dev_addr, u8 reg_addr, u8 bit_index, u8 data);
extern u8   IIC_WriteBits(u8 dev_addr, u8 reg_addr, u8 bit_start, u8 bit_len,
                          u8 data);
extern u8   IIC_WriteByte(u8 dev_addr, u8 reg_addr, u8 data);
extern u8   IIC_WriteBytes(u8 dev_addr, u8 reg_addr, u8 byte_nums, u8 *data);
extern u8   IIC_WriteOneByte(u8 byte);

其中如下函数是基础功能函数,用来实现IIC协议的基础操作:

1
2
3
4
5
6
extern void IIC_Init(void);
extern void IIC_SendAckSignal(void);
extern void IIC_SendStartSignal(void);
extern void IIC_SendStopSignal(void);
extern void IIC_SendNAckSignal(void);
extern void IIC_WaitAckSignal(void);

而后面的函数则使用IIC操作来读写寄存器的内容。这里就不再一一介绍了。

USART

简介

USART即 Universal Synchronous Asynchronous Receiver/Transmitter(通用同步异步串行接收/发送器),这个同步和异步指的是通信的双方的时钟信号是否相同的,如果是相同的话则就是工作在同步模式,否则就是工作在异步模式,同步模式一般都是要求通信双方保持同步时钟序,这也是因为同步通信数据是以一组约定好的字符格式来为定义的,简单来讲同步串口通信传输不以单个字符为基本单位,而是以一个字符组合(帧),帧的长度和数据内容由开发者定义,这个帧中包含有同步信息以通知接收方调整时序进而达到同步,由于数据是以一种包的形式发送的,如果发送和接收方时钟不同步的话,则接收方可能不会从包的起始开始接收,出现错误。而异步通信是以单个字符作为传输的基本单位的,但是为了接收方能够知道数据接收的起始和验证数据在传输过程中没有错误,还需要人为在数据间插入起始位、终止位和校验位。STM32TBU6支持的USART在实际开发过程中主要是和PC,nRF51822进行通信,通信双方的时钟序是不同的,所以目前使用的是 异步串口 进行通信。

固件库操作

USART相关的库函数在 FWLib 目录下的 stm32f10x_usart.cstm32f10x_usart.h 中。

数据发送与接收

STM32串口数据的发送和接收是通过USART_DR寄存器来实现的,该寄存器是一个双寄存器,包含有TDR和RDR,当向该寄存器写数据时,串口就会自动发送,当收到数据的时候,串口就会将数据存储在该寄存器中。固件库中提供的向串口发送数据的函数为:

1
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data);

固件库中提供的从串口读取数据的函数为:

1
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);

串口的状态(数据的接收和发送)是通过读取状态寄存器USART_SR得到的,USART_SR的各位描述如下:

这里值得关注的是第5位的RXNE和第六位的TC,RXNE 位(读数据寄存器非空)被置1的时候,就是提示已经有数据被串口接收到了,并且已经可读了,此时应该尽快读取USART_DR的值,通过读USART_DR可以将该位自动清零,向该位写零也可以达到同样的效果。另外 TC 位(发送完成)被置1的时候,表示USART_DR内的数据已经发送完成了,如果设置了这个位的中断,则会产生中断,该位也有两种清零方式:

1
2
1. USART_SR,写USART_DR
2. 向该位写零
固件库所提供的读取串口状态的函数为:

1
FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG);

这个函数的第一个参数表示要获得状态的串口是哪一个,第二个参数则表示要查看串口的哪种状态,比如要获得串口1的RXNE状态的代码为:

1
USART_GetFlagStatus(USART1, USART_FLAG_RXNE);

一般情况下串口数据的发送和读取都是通过中断的方式来实现的,由于发送是串口的一种主动的操作,即将数据发送到串口上等待被别人读取,其具体实现是先将待发送的数据准备好,然后使能中断发送,此时MCU会通过触发中断的方式来将数据发送并判断发送是否完成,此时需要注意要软件清零。串口接收则直接由MCU获得与串口有关的中断状态,然后查询是否是接收中断。要判断发生的是哪种串口中断,可以固件库提供的函数:

1
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);

比如使能了串口1的接收中断,则当中断发生时,可以通过如下代码查询发生的中断是否是串口接收中断:

1
USART_GetITStatus(USART1, USART_IT_RXNE);

若返回值是SET,则表示该中断发生了。

硬件连接

USART属于 Breeze Mini 主微控制器STM32F103TBU6的通信协议实现,是没有外部电路连接的。

软件设计

环形收发队列

由于STM32的硬件串口上没有设置FIFO缓冲区,为了防止 数据没有被快速读出而导致的被覆盖的问题,此时需要用软件来实现一个FIFO缓冲区,即一个队列。这队列可以将数据暂存在里面,然后按照入队顺序读取数据。

一般队列的C语言实现都是先定义一个数组,然后在该数组上维护一个入队指针和一个出队指针,入队时向右移动入队指针,出队时向右移动出队指针。如果一直重复入队和出队操作,不管数组开的多大,总会出现超出数组的边界的情况,这个是顺序队列的缺陷。但是如果考虑到每次进行串口数据读取的暂存数据都不会太多,即 任意时刻队列中的数据都是不多的 ,而且向前面那种移动指针的方式会使得出队指针之前的空间被永久弃用了,为了解决这个问题,我们可以将队列的头尾相连,这样让入队和出队指针都可以在一个环中循环移动,使得出队指针之前的空间也得到了再次利用。这里简单介绍一下如何使用C语言特性去实现这个循环队列,首先可以定义一个循环队列的数据类型:

1
2
3
4
5
6
7
typedef struct
{
    u8  *buffer;    //指向存储数组的指针变量
    u16  mask;      //队列的大小
    vu16 index_rd;  //出队指针(变量)
    vu16 index_wt;  //入队指针(变量)
} USART_RingBuffer;

其他对于循环队列的操作比较简单,

初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void UART1_init(u32 pclk2, u32 bound)
{
    float temp;
    u16 mantissa;
    u16 fraction;
    temp = (float)(pclk2 * 1000000) / (bound * 16); //得到USARTDIV
    mantissa = temp;                                //得到整数部分
    fraction = (temp - mantissa) * 16;              //得到小数部分
    mantissa <<= 4;
    mantissa += fraction;
    RCC->APB2ENR  |= 1<<2;                          //使能PORTA口时钟
    RCC->APB2ENR  |= 1<<14;                         //使能串口时钟
    GPIOA->CRH    &= 0XFFFFF00F;                    //IO状态设置
    GPIOA->CRH    |= 0X000008B0;                    //IO状态设置
    RCC->APB2RSTR |= 1<<14;                         //复位串口1
    RCC->APB2RSTR &= ~(1<<14);                      //停止复位

    USART1->BRR  = mantissa;                        //波特率设置
    USART1->CR1 |= 0X200C;                          //1位停止,无校验位
    USART1->CR1 |= 1<<8;                            //PE中断使能
    USART1->CR1 |= 1<<5;                            //接收缓冲区非空中断使能
    NVIC_InitUSART();
}

以下代码主要是对接收和发送缓冲队列进行初始化,特别要说明的一点是成员变量 mask 的值设置成 USART_BUFFER_SIZE - 1,是因为USART_BUFFER_SIZE的值设置成了128,对应的二进制位 0000 0000 1000 0000,为了能够通过 位与运算实现接收,发送指针每到达数组边界时就自动回到开始位置,需要保证 mask 的值为 0000 0000 0111 1111 这种形式的,即值为128 - 1 = 127,实现循环移动指针的操作读者可以自己尝试几个值来试一下:

1
2
3
4
5
6
7
8
9
USART_RingBufferRxStructure.index_rd = 0;
USART_RingBufferRxStructure.index_wt = 0;
USART_RingBufferRxStructure.mask     = USART_BUFFER_SIZE - 1;
USART_RingBufferRxStructure.buffer   = &usart_ring_buffer_rx[0];

USART_RingBufferTxStructure.index_rd = 0;
USART_RingBufferTxStructure.index_wt = 0;
USART_RingBufferTxStructure.mask     = USART_BUFFER_SIZE - 1;
USART_RingBufferTxStructure.buffer   = &usart_ring_buffer_tx[0];

串口打印支持

 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
/* Add the below code to support 'printf' function */
#if 1
#pragma import(__use_no_semihosting)

/* The support function is needed by standard library */
struct __FILE
{
    int handle;
};

FILE __stdout;

/* Define _sys_exit() to avoid to use semihosting */
_sys_exit(int x)
{
    x = x;
}

/* Redefine 'fputc' function */
int fputc(int ch, FILE *f)
{
    while ((USART1->SR & 0x40) == 0);  /* Cyclic send until complete */
    USART1->DR = (u8)ch;
    return ch;
}

#endif

有的时候为了能够将MCU的相应状态和数据通过串口打印到电脑等上位机上,可以通过自行编写发送函数来实现,但是由于发送的数据的类型和格式比较多,都自己实现的话比较繁琐。还有一种方法就是通过 重写fputc函数 来实现。这就是所谓的 "串口打印",这种方法调用printf来格式化输出,和C语言中的printf的使用方法是完全一样的,只不过是将输出口从原先的标准I/O重定向到串口上,而由于printf函数底层上是通过 调用fputc实现的,所以需要重写fputc函数。

注意

由于嵌入式设备FLASH空间比较小,所以其实嵌入式C语言并没有实现标准C中的所有功能,比如嵌入式C不支持标准浮点数等,这样如果使用printf输出浮点数会出现问题,此时可以在链接中间代码时使用Keil MDK提供的MicroLIB库,该库提供了很多对嵌入式C功能的补充。

以下是操作USART进行数据读写的函数接口:

1
2
3
4
5
6
7
8
extern void USART_ClearBuffer(USART_RingBuffer *ring_buffer);
extern void USART_InitUSART(u32 baud_rate);
extern void USART_InitUSART1(u32 baud_rate);
extern void USART_SendBuffer(u8 *bytes, u8 length);
extern void USART_SendByte(u8 byte);
extern void USART_WriteBuffer(USART_RingBuffer *ring_buffer, u8 byte);
extern u8   USART_ReadBuffer(USART_RingBuffer *ring_buffer);
extern u16  USART_CountBuffer(USART_RingBuffer *ring_buffer);

每个函数的实现都不是太复杂,这里就不再一一介绍了。

总结

IIC和USART是 Breeze Mini 内外设之间进行数据交换的重要方式,其中IIC主要用于获得支持IIC协议的传感器的数据,而USART则主要用于和上位机通信,打印调试信息和进行蓝牙数据通信。

参考