前言
最近在看一些秋招的笔试和面试题,刚好看到一个老哥的经验贴,他面试的时候被问到了如果芯片串口资源不够了该怎么办?其实可以用IO口来模拟串口,但我之前也没有具体用代码实现过,借此机会用32开发板上的两个IO口来实现串口的功能,实现开发板和串口调试助手两者间数据的收发。
一:协议、硬件相关
为了方便,我这里就只有一位起始位,数据位是8位,一位停止位,没有奇偶校验位和流控,波特率是9600。
开发板我使用的是普中的一块32开发板,主控是stm32f103zet6,使用PB9模拟TX,PE0模拟RX,然后通过usb转串口模块和电脑相连。
具体的接线实物图如下:
二:TX、RX模拟
具体的32工程文件我放到了仓库里,完整的代码都在里面,接下来我就是解释一下编写的逻辑和一些注意点,工程模板是通过正点32的历程修改得到的。
门牙会稍息 / GPIO模拟UART · GitCode
IO模拟UART相关的内容我单独写到了一个.h和.c文件中。
myprintf.h文件中就是IO的一些宏定义和函数声明
#ifndef __MYPRINTF_H
#define __MYPRINTF_H
#include "./SYSTEM/sys/sys.h"
#define TX_GPIO_PORT GPIOB
#define TX_GPIO_PIN GPIO_PIN_9
#define TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB口时钟使能 */
#define RX_GPIO_PORT GPIOE
#define RX_GPIO_PIN GPIO_PIN_0
#define RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0) /* PE口时钟使能 */
#define RX_INT_IRQn EXTI0_IRQn
#define RX_INT_IRQHandler EXTI0_IRQHandler
#define Set_TX(x) do{ x ? \
HAL_GPIO_WritePin(TX_GPIO_PORT, TX_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(TX_GPIO_PORT, TX_GPIO_PIN, GPIO_PIN_RESET); \
}while(0)
#define Get_RX() HAL_GPIO_ReadPin(RX_GPIO_PORT, RX_GPIO_PIN)
void myuart_init(void);
void send_byte(uint8_t data);
void send_str(char *dat);
void myprintf(char *fmt, ...);
#endif
myprintf.c 文件中内容
/**
*/
#include "./BSP/MYPRINTF/myprintf.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#include "./BSP/TIMER/btim.h"
//开始接收数据标志
volatile unsigned char uartStartFlag = 0;
//串口接收缓存
unsigned char uartBuf[256] = {0};
unsigned char uartBufLen = 0;
unsigned char uartHaveDat = 0;
//超时错误处理
volatile unsigned int uartBufTimeout = 0;
volatile unsigned int uartBufStartTimeout = 0;
void myuart_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
TX_GPIO_CLK_ENABLE();
gpio_init_struct.Pin = TX_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(TX_GPIO_PORT, &gpio_init_struct);
RX_GPIO_CLK_ENABLE();
gpio_init_struct.Pin = RX_GPIO_PIN;
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Mode = GPIO_MODE_IT_FALLING;
HAL_GPIO_Init(RX_GPIO_PORT, &gpio_init_struct);
HAL_NVIC_EnableIRQ(RX_INT_IRQn);
Set_TX(0);
}
void send_byte(uint8_t data){
Set_TX(0);
delay_us(104);
for(int i = 0; i < 8; i++){
if(data & 0x01){
Set_TX(1);
}
else{
Set_TX(0);
}
delay_us(104);
data = data >> 1;
}
Set_TX(1);
delay_us(104);
}
void send_str(char *dat){
for(int i = 0; i < strlen(dat); i++){
send_byte(dat[i]);
}
}
void myprintf(char *fmt, ...){
va_list ap;
char string[512];
va_start(ap, fmt);
vsprintf(string, fmt, ap);
send_str(string);
va_end(ap);
}
void RX_INT_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(RX_GPIO_PIN); /* 调用中断处理公用函数 清除KEY0所在中断线 的中断标志位 */
__HAL_GPIO_EXTI_CLEAR_IT(RX_GPIO_PIN); /* HAL库默认先清中断再处理回调,退出时再清一次中断,避免按键抖动误触发 */
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if(GPIO_Pin == RX_GPIO_PIN){
if(uartStartFlag == 0){
uartStartFlag = 1;
btim_timx_int_init(52 - 1, 72 - 1, BTIM_TIM6_INT); //52us接收数据
}
}
}
TX发送内容解释
1:发送的时候为了模拟时序图,需要延时,因为我设置通信波特率为9600,即一个高低电平持续时间就约为104us。
2:发送时数据先发送低位,所以发送一个byte的时候数据需要右移
3:有了发送字节函数(send_byte)之后循环调用就可以发送字符串(send_str)了
4:可以通过C语言中的va_list和vsprintf来实现自定义的printf函数
va_list 和vsprintf相关函数原型:
5:最后得到的myprintf函数就可以像使用C语言中的printf函数一样使用了
RX接收数据内容解释
1:配置RX的IO口是默认上拉,然后是外部中断下降沿触发
2:使用两个定时器来完成数据接收工作,Timer6定时52us用于数据接收,Timer7定时10ms用于确定数据是否传输完成。定时器相关配置和中断处理放到了btime.c和btime.h文件中
btime.h文件内容
#ifndef __BTIM_H
#define __BTIM_H
#include "./SYSTEM/sys/sys.h"
/******************************************************************************************/
/* 基本定时器 定义 */
#define BTIM_TIM6_INT TIM6
#define BTIM_TIM6_INT_IRQn TIM6_IRQn
#define BTIM_TIM6_INT_IRQHandler TIM6_IRQHandler
#define BTIM_TIM6_INT_CLK_ENABLE() do{ __HAL_RCC_TIM6_CLK_ENABLE(); }while(0) /* TIM6 时钟使能 */
#define BTIM_TIM7_INT TIM7
#define BTIM_TIM7_INT_IRQn TIM7_IRQn
#define BTIM_TIM7_INT_IRQHandler TIM7_IRQHandler
#define BTIM_TIM7_INT_CLK_ENABLE() do{ __HAL_RCC_TIM7_CLK_ENABLE(); }while(0) /* TIM7 时钟使能 */
/******************************************************************************************/
void btim_timx_int_init(uint16_t arr, uint16_t psc, TIM_TypeDef* Timerx); /* 基本定时器 定时中断初始化函数 */
#endif
btime.c文件内容,里面主要包含了数据的读取,然后放到缓存中,使用了比较多的标志位
#include "./BSP/LED/led.h"
#include "./BSP/TIMER/btim.h"
#include "./BSP/MYPRINTF/myprintf.h"
//开始接收数据标志
extern volatile unsigned char uartStartFlag;
//串口接收缓存
extern unsigned char uartBuf[256];
extern unsigned char uartBufLen;
extern unsigned char uartHaveDat;
//超时错误处理
extern volatile unsigned int uartBufTimeout;
extern volatile unsigned int uartBufStartTimeout;
TIM_HandleTypeDef g_tim6_handle; /* 定时器句柄 */
TIM_HandleTypeDef g_tim7_handle;
void btim_timx_int_init(uint16_t arr, uint16_t psc, TIM_TypeDef* Timerx)
{
if(Timerx == BTIM_TIM6_INT){
g_tim6_handle.Instance = Timerx; /* 通用定时器X */
g_tim6_handle.Init.Prescaler = psc; /* 设置预分频系数 */
g_tim6_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_tim6_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_tim6_handle);
HAL_TIM_Base_Start_IT(&g_tim6_handle); /* 使能定时器x及其更新中断 */
}
else if(Timerx == BTIM_TIM7_INT){
g_tim7_handle.Instance = Timerx; /* 通用定时器X */
g_tim7_handle.Init.Prescaler = psc; /* 设置预分频系数 */
g_tim7_handle.Init.CounterMode = TIM_COUNTERMODE_UP; /* 递增计数模式 */
g_tim7_handle.Init.Period = arr; /* 自动装载值 */
HAL_TIM_Base_Init(&g_tim7_handle);
HAL_TIM_Base_Start_IT(&g_tim7_handle); /* 使能定时器x及其更新中断 */
}
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIM6_INT)
{
BTIM_TIM6_INT_CLK_ENABLE(); /* 使能TIM时钟 */
HAL_NVIC_EnableIRQ(BTIM_TIM6_INT_IRQn); /* 开启ITM6中断 */
}
if (htim->Instance == BTIM_TIM7_INT)
{
BTIM_TIM7_INT_CLK_ENABLE(); /* 使能TIM时钟 */
HAL_NVIC_EnableIRQ(BTIM_TIM7_INT_IRQn); /* 开启ITM7中断 */
}
}
void BTIM_TIM6_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_tim6_handle); /* 定时器中断公共处理函数 */
}
void BTIM_TIM7_INT_IRQHandler(void)
{
HAL_TIM_IRQHandler(&g_tim7_handle); /* 定时器中断公共处理函数 */
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == BTIM_TIM6_INT)//52us接收数据
{
static unsigned char recvStep = 0; //接收步骤
static unsigned char us52Cnt = 0; //用于104us计数
static unsigned char recDat = 0; //接受一个字节
static unsigned char bitCnt = 0; //接收bit位数
if(uartStartFlag == 1){
if(recvStep == 0){//recvStep = 0是起始位检测步骤
us52Cnt++;
if(us52Cnt == 2){
us52Cnt = 0;
if(Get_RX() == 1){//起始位是高电平,是错误的
uartStartFlag = 0;
__HAL_TIM_DISABLE(&g_tim6_handle);
}
else{
recvStep = 1; //起始位正确接收
recDat = 0;
bitCnt = 0;
}
}
}
else if(recvStep == 1){//正确接收到了起始位,现在开始接收8位数据
us52Cnt++;
if(us52Cnt == 2){
us52Cnt = 0;
recDat = recDat >> 1;
if(Get_RX() == 1){//读到的数据为1
recDat |= 0x80;
}
bitCnt++;
if(bitCnt > 7){//8位数据已近接收完
recvStep = 2; //recvStep = 2,准备接收停止位
}
}
}
else if(recvStep == 2){//接收完8位数据后,判断停止位是否正确接收
us52Cnt++;
if(us52Cnt == 2){
us52Cnt = 0;
if(Get_RX() == 1){//读到的数据为1
uartBuf[uartBufLen++] = recDat;
uartBufTimeout = 0;
uartBufStartTimeout = 1;
}
recvStep = 0;
uartStartFlag = 0;
__HAL_TIM_DISABLE(&g_tim6_handle);
}
}
}
__HAL_TIM_CLEAR_IT(&g_tim6_handle, TIM_IT_UPDATE);
}
if (htim->Instance == BTIM_TIM7_INT)//1ms超时处理
{
if(uartBufStartTimeout == 1){
uartBufTimeout++;
if(uartBufTimeout > 10){
uartBufTimeout = 0;
uartBufStartTimeout = 0;
uartHaveDat = 1;
}
}
__HAL_TIM_CLEAR_IT(&g_tim7_handle, TIM_IT_UPDATE);
}
}
3:接收数据的主要流程就是先判断是否有数据发送,有的话就会触发RX的外部中断,打开52us的定时器,uartStartFlag会置1
4:打开52us定时器之后,通过us52Cnt标志位记录到2之后,代表一个高低电平的持续时间达到了104us,即可以判断一个位是高电平还是低电平。之后就是根据recvStep这个标志位来区分判断起始位、数据位、停止位的过程。
5:发送完一个字节之后将数据放到缓存中,开始10ms定时,超过10ms还没有数据来的话,就代表一次数据传输完成uartHaveDat标志位置1。
6:在main中调用相关函数实现数据收发
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/MYPRINTF/myprintf.h"
#include "./BSP/TIMER/btim.h"
#include <string.h>
//串口接收缓存
extern unsigned char uartBuf[256];
extern unsigned char uartBufLen;
extern unsigned char uartHaveDat;
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
led_init(); /* 初始化LED */
myuart_init();
btim_timx_int_init(7200 - 1, 10 - 1, BTIM_TIM7_INT); //1ms超时处理
memset(uartBuf, 0x00, uartBufLen);
while(1){
if(uartHaveDat == 1){
myprintf("%s\n", uartBuf);
memset(uartBuf, 0x00, uartBufLen);
uartBufLen = 0;
uartHaveDat = 0;
}
}
}
最终使用串口调试助手进行验证,可以达到数据收发的效果
总结
以上就是本文的内容了,建议看一下仓库的源码,理解起来会更快一些。