嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 超级精简系列之十五:超级精简的IO模拟UART接收

超级精简系列之十五:超级精简的IO模拟UART接收

前言

前面一篇我们实现了IO模拟UART发送,我们继续来实现UART接收。对于接收底层资源需要一个输入IO且其可配置位下降沿中断,和一个定时器即可。

实现过程

有了发送的实现,我们依葫芦画瓢,按照发送的模式进行实现,只是一个是发送一个是接收。状态机实现的过程是类似的。

启动接收即使能RX引脚的下降沿中断,用于检测起始位。在中断回调中启动定时器延迟1.5个位宽即可采样第一个bit数据。定时器中断回调中进行状态机处理,配置定时器每隔一个bit采样一次,继续处理后续位。直到处理完所有位,然后调用接收回调,并继续下一个循环。

数据结构

和发送类似,只是TX输出引脚改为RX接收,并且需要提供下降沿中断配置接口。

整个数据结构如下

typedef uint8_t (*io_uart_rx_rd_pf)(void); /**< RX读接口 */typedef void (*io_uart_rx_set_eint_pf)(uint8_t enable, void(*)(void*)); /**< RX接口设置下降沿回调 */typedef void (*io_uart_rx_time_set_period_pf)(uint32_t t, void(*)(void*)); /**< 定时器设置周期接口,单位波特率 */typedef void (*io_uart_rx_time_ctrl_pf)(uint8_t onoff); /**< 定时器启停控制接口 */typedef void (*io_uart_rx_cb_pf)(uint8_t val); /**< 接收到字节回调 */typedef void (*io_uart_rx_init_pf)(void); /**< 底层接口初始化 */typedef void (*io_uart_rx_deinit_pf)(void); /**< 底层接口解除初始化 */
/** * \struct io_uart_rx_e * 接收状态枚举 */typedef enum{ IO_UART_RX_STATE_DIS = 0, /**< 停止状态 */ IO_UART_RX_STATE_IDLE = 1, /**< 空闲等待RX下降沿阶段 */ IO_UART_RX_STATE_START = 2, /**< 起始位阶段 */ IO_UART_RX_STATE_DATA = 3, /**< 数据位阶段 */ IO_UART_RX_STATE_PARITY = 4, /**< 校验位阶段 */ IO_UART_RX_STATE_STOP = 5, /**< 停止位阶段 */} io_uart_rx_e;
/** * \struct io_uart_rx_patity_e * 校验枚举 */typedef enum{ IO_UART_RX_PARITY_NONE = 0, /**< 无校验 */ IO_UART_RX_PARITY_ODD = 1, /**< 奇校验 */ IO_UART_RX_PARITY_EVEN = 2, /**< 偶校验 */ IO_UART_RX_PARITY_0 = 3, /**< 固定0校验 */ IO_UART_RX_PARITY_1 = 4, /**< 固定1校验 */} io_uart_rx_patity_e;
/** * \struct io_uart_rx_dev_st * 接收状态机结构体 */typedef struct { io_uart_rx_e state; /**< 接收主状态 */ uint8_t state_s; /**< 接收子状态 */ uint8_t data; /**< 待发送的字节数据 */ uint8_t cal_parity; /**< 计算的校验位 */ io_uart_rx_patity_e parity; /**< 校验 */ uint8_t stop; /**< 停止位 */ uint8_t data_len; /**< 数据长 */ uint32_t baud; /**< 波特率 */ io_uart_rx_rd_pf rx_rd; /**< RD读接口 */ io_uart_rx_set_eint_pf set_int; /**< 设置RX下降沿中断接口 */ io_uart_rx_init_pf init; /**< 接口初始化 */ io_uart_rx_deinit_pf deinit; /**< 接口解除初始化 */ io_uart_rx_time_set_period_pf time_set_period; /**< 定时器设置周期接口,单位uS */ io_uart_rx_time_ctrl_pf time_ctrl; /**< 定时器启停控制 */ io_uart_rx_cb_pf rx_cb; /**< 接收到字节回调 */} io_uart_rx_dev_st;

接口

只需要初始化和解除初始化接口

再加一个控制器启停的接口。

/** * \fn io_uart_rx_ctrl * 启动或停止接收 * \param[in] dev \ref io_uart_rx_dev_st * \param[in] onoff 使能或者停止接收 * \retval 0 成功 * \retval 其他值 失败 */int io_uart_rx_ctrl(io_uart_rx_dev_st* dev, uint8_t onoff);
/** * \fn io_uart_rx_init * 初始化 * \param[in] dev \ref io_uart_rx_dev_st*/void io_uart_rx_init(io_uart_rx_dev_st* dev);
/** * \fn io_uart_rx_deinit * 解除初始化 * \param[in] dev \ref io_uart_rx_dev_st*/void io_uart_rx_deinit(io_uart_rx_dev_st* dev);

实现

初始化解除初始化

void io_uart_rx_init(io_uart_rx_dev_st* dev){ if(dev == 0) { return; } if(dev->init == 0) { return; } dev->init();}
void io_uart_rx_deinit(io_uart_rx_dev_st* dev){ if(dev == 0) { return; } if(dev->deinit == 0) { return; } dev->deinit();}

启动接收,即使能下降沿中检测起始位,检测到下降沿回调io_uart_rx_start并停止外部中断检测,交给定时采样处理。

static void io_uart_rx_start(void* p){ io_uart_rx_dev_st* dev = (io_uart_rx_dev_st*)p; /* 检测到下降沿中断时调用 */ dev->set_int(0,0); /* 关闭外部中间检测 */ dev->time_set_period(dev->baud*2/3,io_uart_rx_handle); /* 设置1.5位宽的周期,周期到了回调io_uart_tx_handle 采样第一个数据 */ dev->time_ctrl(1); dev->state = IO_UART_RX_STATE_DATA; /* 更新状态到数据阶段,直接延迟跳过1个周期的开始位,并且延迟0.5个周期到位中间采样 */ dev->cal_parity = 0; /* 初始化信息 */ dev->state_s = 0; dev->data = 0;}
int io_uart_rx_ctrl(io_uart_rx_dev_st* dev, uint8_t onoff){ if(dev == 0) { return -1; } if(onoff != 0) { dev->set_int(1,io_uart_rx_start); /* 使能外部中间检测 */ dev->state = IO_UART_RX_STATE_IDLE; /* 更新状态 */ } else { dev->set_int(0,0); /* 关闭外部中间检测 */ dev->time_ctrl(0); /* 停止定时器 */ dev->state = IO_UART_RX_STATE_DIS; /* 设置停止状态 */ } return 0;}

状态机处理

这个函数是核心,对照着注释和字节的阶段即可,不再赘述

和发送流程差不多

void io_uart_rx_handle(void* p){ uint8_t bit = 0; uint8_t parity = 0; io_uart_rx_dev_st* dev = (io_uart_rx_dev_st*)p; /* 参数检查 */ if(dev == 0) { return; } switch(dev->state) { case IO_UART_RX_STATE_DATA: dev->time_set_period(dev->baud,io_uart_rx_handle); /* 设置一个位宽的周期,周期到了回调io_uart_rx_handle */ dev->time_ctrl(1);
/* 更新一位,低位在前,所以先放在高位然后往低位移动 */ bit = dev->rx_rd(); dev->data >>= 1; /* 这里要先移动后更新数据,因为本次最后更新的是有效数据 */ if(bit != 0) { dev->data |= 0x80; } dev->cal_parity ^= bit; dev->state_s++; if(dev->state_s >= dev->data_len) /* state_s先加,因为进来一次就是收到一个bit数据 */ { if(dev->parity == IO_UART_RX_PARITY_NONE) { /* 这里就开始等待停止位 */ dev->state = IO_UART_RX_STATE_STOP; dev->state_s = 0; /**< 停止位要用到state_s计数停止位数需要清0 */ } else { /* 这里就需要开始等待校验位 */ dev->state = IO_UART_RX_STATE_PARITY; } } else { /* 不改变状态继续下一位 */ } break; case IO_UART_RX_STATE_PARITY: /* 校验位时间到,进入停止位阶段 这里就需要开始发送停止位 */ dev->time_set_period(dev->baud,io_uart_rx_handle); /* 设置一个位宽的周期,周期到了回调io_uart_rx_handle */ dev->time_ctrl(1);
switch (dev->parity) { case IO_UART_RX_PARITY_ODD: parity = dev->cal_parity ^ 0x01; break; case IO_UART_RX_PARITY_EVEN: parity = dev->cal_parity; break; case IO_UART_RX_PARITY_0: parity = 0; break; case IO_UART_RX_PARITY_1: parity = 1; break; default: break; }
bit = dev->rx_rd(); if(parity != bit) { /* 校验错误处理 */ }
dev->state = IO_UART_RX_STATE_STOP; dev->state_s = 0; /**< 停止位要用到sstate_s计数停止位数需要清0 */ break; case IO_UART_RX_STATE_STOP: dev->state_s++; /* 进入这里实际就已经接收了一个停止位了,所以是先加 */ if(dev->state_s >= dev->stop) { /* 完成一个字节的接收 */ dev->rx_cb(dev->data); dev->set_int(1,io_uart_rx_start); /* 继续下一个字节接收处理 */ dev->state = IO_UART_RX_STATE_IDLE; } else { dev->time_set_period(dev->baud,io_uart_rx_handle); /* 设置一个位宽的周期,周期到了回调io_uart_tx_handle */ /* 不改变状态继续下个停止位 */ dev->time_ctrl(1); } break; }}

移植

移植即实现数据结构中的接口依赖,即下面的接口函数

static io_uart_rx_dev_st uart_rx_dev={ .parity = IO_UART_RX_PARITY_ODD, .stop = 2, .data_len=8, .baud = 115200,
.rx_rd = port_rx_rd, .init = port_rx_init, .deinit = port_rx_deinit, .time_set_period = port_time1_set_period, .rx_cb = rx_done_cb, .time_ctrl = port_timer1_ctrl, .set_int = port_set_int,};

这里基于HPM53xx开发板实现,代码如下

Uart_rx_port.c

#include <rtthread.h>#include <rtdevice.h>#include "rtt_board.h"#include <drv_gpio.h>#include <drv_gpio.h>#include "io_uart_rx.h"
static void port_timer1_cb(void);static void port_time1_set_period(uint32_t period, void(*cb)(void*));static void port_set_int(uint8_t enable, void(*)(void*));static void port_rx_init(void);static void port_rx_deinit(void);static uint8_t port_rx_rd(void);static void port_timer1_ctrl(uint8_t onoff);static void(*t1_cb)(void*) = 0;static void(*int_cb)(void*) = 0;static void rx_done_cb(uint8_t val);static void (*rx_cb)(uint8_t val) = 0;static io_uart_rx_dev_st uart_rx_dev={ .parity = IO_UART_RX_PARITY_ODD, .stop = 2, .data_len=8, .baud = 115200,
.rx_rd = port_rx_rd, .init = port_rx_init, .deinit = port_rx_deinit, .time_set_period = port_time1_set_period, .rx_cb = rx_done_cb, .time_ctrl = port_timer1_ctrl, .set_int = port_set_int,};

static void port_set_int(uint8_t enable, void(*cb)(void*)){ int_cb = cb; if(enable != 0) { HPM_GPIO0->PL[GPIO_DI_GPIOA].SET = (1u << 26); /* 低电平或者下降沿中断 */ HPM_GPIO0->TP[GPIO_DI_GPIOA].SET = (1u << 26); /* 边沿中断 */ HPM_GPIO0->IF[GPIO_DI_GPIOA].VALUE = (1u << 26); /* 写1清除标志 */ HPM_GPIO0->IE[GPIO_DI_GPIOA].SET = (1u << 26); /* 写1使能中断 */ } else { HPM_GPIO0->IE[GPIO_DI_GPIOA].CLEAR = (1u << 26); /* 写1不使能中断 */ }}

static void port_timer1_cb(void){ HPM_GPTMR1->SR = 0x1; /* 写1清除标志 */ if(t1_cb != 0) { t1_cb(&uart_rx_dev); }}
static void port_int_cb(void){ HPM_GPIO0->IF[GPIO_DI_GPIOA].VALUE = (1u << 26); /* 写1清除标志 */ if(int_cb) { int_cb(&uart_rx_dev); }}

static void port_time1_set_period(uint32_t baud, void(*cb)(void*)){ t1_cb = cb; HPM_GPTMR1->CHANNEL[0].CR &= ~(1u << 10); /* 停止 */ HPM_GPTMR1->CHANNEL[0].CR |= (1u << 14); /* 复位 */ HPM_GPTMR1->CHANNEL[0].CR &= ~(1u << 14); /* 释放复位 */
HPM_GPTMR1->CHANNEL[0].CR |= (1u << 3); /* 调试时定时器停止 */
HPM_GPTMR1->CHANNEL[0].RLD = (81000000/baud); /* 重载值 计数器从0开始运行到重载值恢复到0重新计数 1nS对应1/10个计数值 由于软件执行需要时间所以这里的值要小一点 */
HPM_GPTMR1->SR = 0x1; /* 写1清除标志 */
HPM_GPTMR1->IRQEN |= 0x01; /* 通道0重载中断使能 */
}
static void port_rx_init(void){ HPM_IOC->PAD[IOC_PAD_PA26].FUNC_CTL = IOC_PA26_FUNC_CTL_GPIO_A_26; HPM_GPIO0->OE[GPIO_DI_GPIOA].CLEAR = 1u << 26;
install_isr(IRQn_GPTMR1,(uint32_t)port_timer1_cb); intc_m_enable_irq(IRQn_GPTMR1);
install_isr(IRQn_GPIO0_A,(uint32_t)port_int_cb); intc_m_enable_irq(IRQn_GPIO0_A);}
static void port_rx_deinit(void){ //uninstall_isr(IRQn_GPTMR1); intc_m_disable_irq(IRQn_GPTMR1); HPM_GPIO0->IE[GPIO_DI_GPIOA].CLEAR = (1u << 26); /* 写1不使能中断 */}
static uint8_t port_rx_rd(void){ if((HPM_GPIO0->DI[GPIO_DI_GPIOA].VALUE & (1u << 26)) != 0) { return 1; } else { return 0; }}
static void port_timer1_ctrl(uint8_t onoff){ if(onoff) { HPM_GPTMR1->CHANNEL[0].CR |= (1u << 10); /* 使能 */ } else { HPM_GPTMR1->CHANNEL[0].CR &= ~(1u << 10); /* 停止 */ }}
static void rx_done_cb(uint8_t val){ rx_cb(val);}
void sw_uart_rx_init(void){ io_uart_rx_init(&uart_rx_dev);}
void sw_uart_rx_deinit(void){ io_uart_rx_deinit(&uart_rx_dev);}
void sw_uart_rx_start(uint8_t ctrl,void(*cb)(uint8_t val)){ rx_cb = cb; io_uart_rx_ctrl(&uart_rx_dev,ctrl);}

Uart_rx_port.h

#ifndef UART_RX_PORT_H#define UART_RX_PORT_H
#ifdef __cplusplus extern "C"{#endif
#include <stdint.h>
void sw_uart_rx_init(void);void sw_uart_rx_deinit(void);void sw_uart_rx_start(uint8_t ctrl,void(*cb)(uint8_t val));
#ifdef __cplusplus }#endif
#endif

测试

配置参数为115200-8-ODD-2

static io_uart_tx_dev_st uart_tx_dev={ .parity = IO_UART_TX_PARITY_ODD, .stop = 2, .data_len=8, .baud = 115200,
.tx_wr = port_tx_wr, .init = port_tx_init, .deinit = port_tx_deinit, .time_set_period = port_time0_set_period, .tx_cb =tx_done_cb, .time_ctrl = port_timer0_ctrl,};
static io_uart_rx_dev_st uart_rx_dev={ .parity = IO_UART_RX_PARITY_ODD, .stop = 2, .data_len=8, .baud = 115200,
.rx_rd = port_rx_rd, .init = port_rx_init, .deinit = port_rx_deinit, .time_set_period = port_time1_set_period, .rx_cb = rx_done_cb, .time_ctrl = port_timer1_ctrl, .set_int = port_set_int,};

测试代码如下,收到数据原样返回。

uint8_t str[]={0xAA,0xAA};
volatile int rx_flag = 0;void rx_cb(uint8_t val){ str[0]= val; rx_flag = 1;}void thread_entry(void *arg){ sw_uart_tx_init(); sw_uart_rx_init(); sw_uart_rx_start(1,rx_cb); while(1){ if(rx_flag) { rx_flag = 0; sw_uart_tx_byte(str[0]); } } sw_uart_tx_deinit(); sw_uart_rx_deinit();}

使用串口调试助手发送,开发板返回,看到完全正确。使用示波器产看波形也完全正确。

总结

以上超级简单的代码实现了串口数据的接收,具备良好的可移植性,这样我们就完成了IO模拟串口的全部实现。但是这还不够,以上示例代码仅作测试,并不适合实际应用。在实际应用中,我们还可以继续基于上面一篇的FIFO实现,实现基于环形缓冲区的串口收发驱动,这样才能实现高效方便应用调用的接口,见下一篇。