软件模拟串口通信

有时候受到单片机引脚资源问题,不得不使用GPIO口来模拟串口通信,本代码是是半双工的串口通信。
串口配置:
波特率:115200

数据长度:8

起始位:1

停止位:1
 

GPIO口的配置:
TX推荐使用推挽输出,RX是外部中断的方式,下降沿触发;

void MX_GPIO_Init(void)
{

  LL_EXTI_InitTypeDef EXTI_InitStruct = {0};
  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOH);
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);
  LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC);

  /**/
  LL_GPIO_SetOutputPin(USART2_TX_GPIO_Port, USART2_TX_Pin);

  /**/
  GPIO_InitStruct.Pin = USART2_TX_Pin;
  GPIO_InitStruct.Mode = LL_GPIO_MODE_OUTPUT;
  GPIO_InitStruct.Speed = LL_GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  LL_GPIO_Init(USART2_TX_GPIO_Port, &GPIO_InitStruct);

  /**/
  LL_SYSCFG_SetEXTISource(LL_SYSCFG_EXTI_PORTC, LL_SYSCFG_EXTI_LINE11);

  /**/
  LL_GPIO_SetPinPull(USART2_RX_GPIO_Port, USART2_RX_Pin, LL_GPIO_PULL_NO);

  /**/
  LL_GPIO_SetPinMode(USART2_RX_GPIO_Port, USART2_RX_Pin, LL_GPIO_MODE_INPUT);

  /**/
  EXTI_InitStruct.Line_0_31 = LL_EXTI_LINE_11;
  EXTI_InitStruct.LineCommand = ENABLE;
  EXTI_InitStruct.Mode = LL_EXTI_MODE_IT;
  EXTI_InitStruct.Trigger = LL_EXTI_TRIGGER_FALLING;
  LL_EXTI_Init(&EXTI_InitStruct);

  /* EXTI interrupt init*/
  NVIC_SetPriority(EXTI15_10_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(),1, 0));
  NVIC_EnableIRQ(EXTI15_10_IRQn);

}

定时器的配置:分配系数选择不分频,我查AI是说分频系数小,自动重装载值大的情况下比较准;
定时器的时钟源是60MHZ,所以大概是4.35us,是115200波特率下时序的一半;

void MX_TIM2_Init(void)
{

  /* USER CODE BEGIN TIM2_Init 0 */

  /* USER CODE END TIM2_Init 0 */

  TIM_ClockConfigTypeDef sClockSourceConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM2_Init 1 */

  /* USER CODE END TIM2_Init 1 */
  htim2.Instance = TIM2;
  htim2.Init.Prescaler = 0;
  htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim2.Init.Period = 260;
  htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim2) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM2_Init 2 */

  /* USER CODE END TIM2_Init 2 */

}

使用的变量和宏定义:

#define	BAUD_115200_Half	260
#define BAUD_115200	520
#define	Idle 0
#define	Buys 1
#define Mode_RX 0
#define Mode_TX 1
bool isFirstEntry_115200 = false;
uint8_t rxData = 0;          // 接收的数据字节
uint8_t rxBitPos = 0;       // 当前bit位置(0~9:起始位+8数据位+停止位)
uint8_t rxReceiving = Idle;     // 0=空闲,1=接收中
uint8_t rxDataBuff[60] = {0};
uint8_t txDataBuff[60] = {0};
uint8_t rxIdex = 0;
uint8_t USARTStart = Mode_RX;   //默认为接收状态
bool sendPermission = false;    //开始发送标志位
uint8_t temp = 0;

main函数:

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */
char *test_str = "AT+UART_CUR=9600,8,1,0,0\r\n";
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    if(rxIdex == 50){
      rxIdex = 0;
    }
		if(temp <= 1){
			HAL_Delay(50);
			temp++;
			txDataBuff[0] = 0x41;
			txDataBuff[1] = 0x54;	
			txDataBuff[2] = 0x0D;
			txDataBuff[3] = 0x0A;
			SendByte(txDataBuff, 4);
		}
		if(temp == 2){
			temp++;
			HAL_Delay(50);
			StrToASCII(test_str, txDataBuff, sizeof(txDataBuff));
			SendByte(txDataBuff, 26);
		}
  }
  /* USER CODE END 3 */
}

StrToASCII函数负责把字符串转成对应的ascii码的16进制形式;

SendByte函数是通过模拟串口发送一个byte的函数

SendByte函数:

void SendByte(uint8_t *DataBuff, uint8_t Lenght)
{
  USARTStart = Mode_TX;   //发送模式
  __HAL_TIM_SET_AUTORELOAD(&htim2, BAUD_115200);
  __HAL_TIM_SET_COUNTER(&htim2, 0);  // 将定时器2的计数器值清零
  __HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);		//清除中断标志位
  HAL_TIM_Base_Start_IT(&htim2);    //开启定时器中断
  HAL_TIM_Base_Start(&htim2);				//开启定时器
  for(uint8_t i = 0; i < Lenght; i++){
    SendBit(DataBuff[i]);
  }
  HAL_TIM_Base_Stop_IT(&htim2);
  USARTStart = Mode_TX;   //接收模式
	__HAL_TIM_SET_AUTORELOAD(&htim2, BAUD_115200_Half);
}

解析:
USARTStar:是用于在进入定时器中断的时候判断现在要操作的是发送还是接受模式,在定时器中断回调函数里面使用;

1、首先把串口模式置为发送模式;

2、定时器的自动重装载值设为520(BAUD_115200),定时器计数值清零,清除中断标志位(防止开中断就马上进入中断),开始定时器中断,开启定时器;

3、进入SendBit函数里面发送数据,SendBit函数相当于SendByte的子函数,因为数据是1bit的方式发送的。

4、发送完成后关闭定时器中断,并且重置串口模式为接收模式,并且把定时器的自动重装载值设为260。这一步的目的是因为发送 数据我们是可控的,但是接收数据的时机我们无法知晓,所以串口的模式默认要为接收模式,否则串口数据来的时候会错过数据。

看到这里可能会有人产生疑问,定时器为什么要设为115200时序的一半呢,而不是吻合115200时序?这个在串口接收的代码部分会做解释;

SendBit函数:

void SendBit(uint8_t Data)
{
  // 循环10次:对应10位(1起始+8数据+1停止)
  for(uint8_t i = 0; i < 10; i++)
  {
    while (1)
    {
      if(sendPermission == true)  // 等待定时器中断(1个波特率周期)
      {
        if(i == 0)  // 第0位:起始位(低电平)
        {
          LL_GPIO_ResetOutputPin(USART2_TX_GPIO_Port, USART2_TX_Pin);
        }
        else if(i == 9)  // 第9位:停止位(高电平)
        {
          LL_GPIO_SetOutputPin(USART2_TX_GPIO_Port, USART2_TX_Pin);
        }
        else  // 第1~8位:数据位(LSB先传,i=1对应bit0,i=8对应bit7)
        {
          if(Data & (0x01 << (i-1)))  // 取数据的第(i-1)位
          {
            LL_GPIO_SetOutputPin(USART2_TX_GPIO_Port, USART2_TX_Pin);
          }
          else
          {
            LL_GPIO_ResetOutputPin(USART2_TX_GPIO_Port, USART2_TX_Pin);
          }
        }
        sendPermission = false;  // 处理完当前位,等待下一个周期
        break;
      }
    }
  }
}

解释:
sendPermission变量是发送运行的标志位,这个标志位在定时器中断回调函数里面会置true,也就每当需要发送1bit的数据时,都需要等待定时器延迟8.68us(与115200波特率时序吻合),之后才能发送该bit数据,从而达到符合115200时序的要求。发完1byte数据会退出该函数。

定时器中断回调函数:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM2)
  {
    switch (USARTStart)
    {
    case 0: // 接收模式
      if(isFirstEntry_115200 == false){
        isFirstEntry_115200 = true;
        __HAL_TIM_SET_AUTORELOAD(&htim2, BAUD_115200);
      }
      
      if (rxReceiving)
      {
        uint8_t currentLevel = LL_GPIO_IsInputPinSet(USART2_RX_GPIO_Port, USART2_RX_Pin);
        
        if (rxBitPos == 0)  // 起始位
        {
          if (currentLevel != 0)  // 起始位应该是低电平
          {
            // 误触发,终止接收
            rxReceiving = Idle;
            HAL_TIM_Base_Stop_IT(htim);
            HAL_Delay(3);
            LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_11);
            break;
          }
        }
        else if (rxBitPos >= 1 && rxBitPos <= 8)  // 数据位
        {
          rxData |= (currentLevel << (rxBitPos - 1));
        }
        else if (rxBitPos == 9)  // 停止位
        {
          if (currentLevel == 1)  // 停止位应该是高电平
          {
            if(rxIdex < sizeof(rxDataBuff))  // 防止缓冲区溢出
            {
              rxDataBuff[rxIdex++] = rxData;
            }
          }
          // 结束接收
          rxReceiving = Idle;
          rxData = 0;
          rxBitPos = 0;
          isFirstEntry_115200 = false;
          __HAL_TIM_SET_AUTORELOAD(&htim2, BAUD_115200_Half);
          HAL_TIM_Base_Stop_IT(htim);
          LL_EXTI_EnableIT_0_31(LL_EXTI_LINE_11);
        }
        
        rxBitPos++;
      }
      break;

    case 1: // 发送模式
      sendPermission = true;
      break;

    default:
      break;
    }
  }
}

我们以发送数据为例解释:

要注意的是,定时器默认的自动重装载值是115200时序的一半;

这样有两个好处,第一就是当起始位来的时候,触发外部中断启动定时器,在该时序的一半时进入中断,并进行采样确认是不是真的是起始条件,避免被干扰误触。

第二就是,在起始条件采样的时候实在该时序的中间进行采样,我第一次进入定时器中断回调函数的时候,就会把定时器的自动重装载值置520(吻合115200时序),随后的每一次采样都可以在时序中间采样,避免漏采或者被干扰;避免漏采很重要,如果一开始时序和115200吻合,也就是说每一个bit结束的时候你才进行采样,很容易漏采的。

首先是接收起始位,这个没啥好说的。

随后由定时器触发中断采集8bit的数据,还有采集1bit的停止位,采集到停止位后视为结束这1byte的数据接收。随后关闭定时器中断,重新开启外部中断。

最后解释外部中断回调函数函数:

void USART2_RX_IRQ(void)
{
  if (!rxReceiving)  // 仅空闲时响应
  {
		__HAL_TIM_SET_COUNTER(&htim2, 0);  // 将定时器2的计数器值清零
      rxReceiving = Buys;
      rxBitPos = 0;
      rxData = 0;
      
      // 关闭外部中断,避免干扰
      LL_EXTI_DisableIT_0_31(LL_EXTI_LINE_11);
      
      // 启动定时器(HAL库函数,假设定时器已初始化)
      HAL_TIM_Base_Start_IT(&htim2);  // htim2是定时器句柄
  }
}

//这个中断服务函数是在stm32f2xx_it.c文件里面的
void EXTI15_10_IRQHandler(void)
{
  /* USER CODE BEGIN EXTI15_10_IRQn 0 */

  /* USER CODE END EXTI15_10_IRQn 0 */
  if (LL_EXTI_IsActiveFlag_0_31(LL_EXTI_LINE_11) != RESET)
  {
    LL_EXTI_ClearFlag_0_31(LL_EXTI_LINE_11);
    /* USER CODE BEGIN LL_EXTI_LINE_11 */
    USART2_RX_IRQ();
    /* USER CODE END LL_EXTI_LINE_11 */
  }
  /* USER CODE BEGIN EXTI15_10_IRQn 1 */

  /* USER CODE END EXTI15_10_IRQn 1 */
}

解析:

触发外部中断的时候把定时器的计数值清零;然后重置一些接收要用到的变量;之后再启动定时器中断;

到这里就是能实现整个软件模拟串口进行通讯了;

附上字符串转ASCII码的16进制代码吧:

uint16_t StrToASCII(const char *str, uint8_t *ascii_buf, uint16_t buf_len)
{
    if (str == NULL || ascii_buf == NULL || buf_len == 0)
        return 0;

    uint16_t str_len = strlen(str);  // 获取字符串长度(不含结束符)
    uint16_t convert_len = (str_len < buf_len) ? str_len : buf_len;

    // 逐个字符转换为ASCII码
    for (uint16_t i = 0; i < convert_len; i++)
    {
        ascii_buf[i] = (uint8_t)str[i];  // 字符直接强转为uint8_t即得ASCII值
    }

    return convert_len;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值