stm32使用io口模拟串口通信
好久没写代码了,无聊再回顾回顾hal库的开发知识,所以使用io口模拟了串口通信。使用的是stm32f103c8t6,主要使用的就是俩gpio口、系统滴答定时器Systick用来延时、TIM2用来定时采样数据。
一、GPIO初始化TX和RX
使用PA1模拟RX,PA2模拟TX,由于串口通信空闲状态下为高电平,所以输入口配置为上拉输入
/*
Function: io模拟串口初始化函数
*/
void MY_UART_INIT(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio_init_struct;
//配置tx PA2
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP; //上拉
gpio_init_struct.Pin = TX_PIN;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
//配置rx PA1
gpio_init_struct.Mode = GPIO_MODE_INPUT;
gpio_init_struct.Pin = RX_PIN;
HAL_GPIO_Init(GPIOA, &gpio_init_struct);
}
二、通过GPIO口模拟TX发送
发送的主要思路就是按数据帧格式发送,其中先发送起始位(低电平表示起始),然后发送8位数据位,最后发送停止位(高电平代表停止)。按照提前约定好的波特率,如本文设置1200波特率,则表示1秒发送1200个bit位,那么一个bit位的发送时间间隔就是1/1200=0.000833s,也就是833us,因此每发送一个bit位,延时833us,延时函数采用滴答定时器。
/*
Function: 采用系统滴答定时器实现1us延时函数
*/
void delay_us(uint32_t nus) {
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000000);
HAL_Delay(nus - 1);
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / 1000);//延时完再改回去1ms
}
//系统滴答定时器的中断函数
void SysTick_Handler(void)
{
HAL_IncTick();
}
/*
Function: 串口发送字节函数
*/
void send_byte(uint8_t data) {
set_tx(GPIO_PIN_RESET);//起始信号
delay_us(833);//波特率为1200
uint8_t count = 0;
//低位发起
while (count < 8) {
if (data & 0x01)
set_tx(GPIO_PIN_SET);
else
set_tx(GPIO_PIN_RESET);
delay_us(833);
data >>= 1;
count++;
}
set_tx(GPIO_PIN_SET);//截止信号
delay_us(833);
}
/*
Function: 串口发送字符串函数
*/
void send_str(uint8_t *str) {
while (*str != '\0') {
send_byte(*str);
str++;
}
}
三、定时器初始化
定时器主要是用来采集RX的数据,预分频系数和重装载值根据波特率来配置,因为上面已经讲到1200波特率对应的采样间隔是833us,所以设置72分频,重装载值为833。至于计算的话,具体是:定时器2挂载在APB1总线上,时钟频率是72MHz,所以72分频后,每us计数1,重装载设置为833,就是每隔833us进入一次中断,可以在中断中对RX传输的每个bit进行采样,组合为实际的值,放入缓冲区中。
/*
Function: 定时器2初始化
*/
void TIM2_Init(void)
{
htim2.Instance = TIM2;
htim2.Init.Prescaler = 72 - 1;
htim2.Init.Period = 833 - 1;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2);
}
/*
Function: 定时器2初始化回调函数
*/
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim) {
__HAL_RCC_TIM2_CLK_ENABLE();
//设置定时器中断的中断优先级
HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
四、定时器中断采样RX接收数据
设置好定时器预分频系数和重装载值后,在中断函数中采样RX每个bit,然后组合为实际的byte,放入缓冲区中。
/*
Function: TIM2定时器中断处理函数
*/
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim2);
}
//在中断处理函数中,识别每个bit的位置和状态,放入局部static变量data中
//一个完整的byte接收完后,放入缓冲区cache中
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM2) {
uint8_t temp = HAL_GPIO_ReadPin(RX_GPIOx, RX_PIN); //读取rx线的数据
static uint8_t data = 0;
static uint8_t cnt = 0;
if (state == state_stop && !temp) { //下降沿且上一帧已结束,则表示新的帧开始
state = state_start;
} else if (state == state_start && cnt < 8) {
if (temp) data |= (0x01 << cnt);
else data &= ~(0x01 << cnt);
cnt++;
} else if (cnt == 8 && temp) { //停止位是高电平才算正常结束
state = state_stop;
cache[cache_len++] = data;
data = 0;
cnt = 0;
} else {
//此帧有问题,另作处理,这里不考虑有问题...
}
}
}
五、主函数
主函数中通过io模拟串口接收上位机传来的数据,并通过io口模拟串口发送出去
/******************************************************************
author: luo jincheng
date: 2024/09/08
description:
1: 本文件是主程序入口,使用io口模拟串口通信
2: 程序逻辑是上位机发送数据,程序收到后原样返回
******************************************************************/
extern uint8_t cache_len;
extern uint8_t cache[200];
int main(void) {
HAL_Init();
SystemClock_Config();
TIM2_Init();
MY_UART_INIT();
while (1) {
//缓冲区不为空时(即接收到数据了),将缓冲区数据发送给上位机
if (cache_len > 0) {
for (int i = 0; i < cache_len; i++) {
send_byte(cache[i]);
}
cache[0] = '\0';
cache_len = 0;
}
}
}
六、测试结果
测试通过
作者:罗金承