ESP-IDF下的软件串口softserial库
前言
最近在鼓捣esp-idf,想把原本arduino的项目重构到idf框架下,但是在第一步我就犯了难,我发现没有一个idf写的软件串口库可以用,我就寻思着自己写了一个。现在已经完成最简单版本的搭建,实现了基础功能(现只验证了esp-idf v5.21 + esp32s3,高波特率下并不稳定,现只支持8N1)。由于本身实力不足,并且是第一次写开源库,存在许多错误,还请大家批评指正。
项目网址 : https://github.com/1StarStarFire1/ESPIDF-Softserial-UART
回顾串口时序
以最常用的8-N-1为例子(八数据位,一位停止位,零位校验位,正常逻辑电平),其中各个位的持续时间为:1/波特率。 空闲状态为高电平,起始位为低电平,停止位为高电平,D0-D7低位先行,高位在后。
时间处理
为了实现精确的控制,我们通过波特率计算每一个数据位的CPU周期
/*
* 计算每位时间(CPU周期)
* @param baud_rate 波特率
* @return 每位对应CPU周期数
* @note 乘以2是分频机制
*/
uint64_t calculate_bit_time(uint32_t baud_rate)
{
//uint64_t bit_time_us = (1000000UL / baud_rate); //原本计算会有四舍五入积累误差,故舍去
return 2 * (CPU_CLK_FREQ / baud_rate); // 转换为CPU周期数
//CPU_CLK_FREQ = 80Mhz 但是 cpu经过倍频 其真正频率为 = 160Mhz 通过使用 esp_cpu_get_cycle_count()可以验证其频率
}
通过获取当前CPU时序,进行时间阻塞式延迟(高精度要求,不适合使用实时操作系统),其中bit_cycles为calculate_bit_time()的计算结果
/*
* 精确等待指定CPU周期
* @param star 指向起始时间的指针(自动更新)
* @param bit_cycles 需要等待的周期数
* @notestar 变为+=bit_cycles
*/
void wait_time(uint64_t* star , uint64_t bit_cycles)
{
uint64_t end = *star + bit_cycles;
while(esp_cpu_get_cycle_count() < end);
*star = end;
}
接收处理
为了实现数据接收,我们根据时序图进行如下布置
部分代码如下
/*
* 接收中断服务例程(ISR)
* @param arg 软件UART结构体指针
* @note 必须在临界区中处理中断
*/
void IRAM_ATTR software_uart_rx_isr(void* arg)
{
SoftwareUART* uart = (SoftwareUART*)arg;
portBASE_TYPE higher_priority_task_woken = pdFALSE;
// 进入临界区
portENTER_CRITICAL_ISR(&uart->lock);
// 检测起始位(下降沿触发)
if (gpio_get_level(uart->rx_pin) == 0)
{
uint8_t data = 0;
uint64_t star = esp_cpu_get_cycle_count();
wait_time(&star , (uart->bit_cycles)/3);//除以二保证在bit中位读取数据
if(gpio_get_level(uart->rx_pin) == 0)//起始位
{
// 读取8位数据(LSB到 MSB)
for (int i = 0; i < 8; i++)
{
wait_time(&star , uart->bit_cycles); // 等待至采样点
data |= (gpio_get_level(uart->rx_pin) << i);
// 等待下一周期
}
// 检查停止位(应为高电平)
wait_time(&star , uart->bit_cycles);
if (gpio_get_level(uart->rx_pin) == 1) //停止位
{
// 存入缓冲区
uint16_t next_head = (uart->rx_buffer.head + 1) % uart->rx_buff_size;
if (next_head != uart->rx_buffer.tail)
{
uart->rx_buffer.buffer[uart->rx_buffer.head] = data;
uart->rx_buffer.head = next_head;
}
}
}
}
portEXIT_CRITICAL_ISR(&uart->lock);
}
发送处理
数据发送的思想与发送几乎一致,如下图
部分代码如下
/*
* 发送一个字节(8位数据 + 起始位 + 停止位)
* @param uart 软件UART结构体指针
* @param data 需要发送的8位数据
*/
void software_uart_tx_byte(SoftwareUART *uart, uint8_t data)
{
portENTER_CRITICAL(&uart->lock);
uint64_t star = esp_cpu_get_cycle_count();
// 发送起始位(低电平)
gpio_set_level(uart->tx_pin, 0);
//持续一个bit
wait_time(&star , uart->bit_cycles);
// 发送8位数据(LSB先发送)
for (int i = 0; i < 8; i++)
{
gpio_set_level(uart->tx_pin, (data >> i) & 0x01);
//持续一个bit
wait_time(&star , uart->bit_cycles);
}
// 发送停止位(高电平)
gpio_set_level(uart->tx_pin, 1);
//持续一个bit
wait_time(&star , uart->bit_cycles);
portEXIT_CRITICAL(&uart->lock);
}
想要更详尽的代码,请点击文章开头连接。感谢各位阅读。