【STM32】使用HAL库的“DMA+空闲中断”实现环形缓冲区进行串口收发数据(含详细思路)_stm32 hal dma中断
文章目录
- 前言
- 一、DMA是什么?
- 二、相关配置
- 三、DMA工作模式
-
- 【1】直传模式(Direct Mode)
- 【2】FIFO模式(FIFO Mode)
-
- 友情提醒
- 介绍
- 普通模式下
- 循环模式下
- FIFO Mode具体分析
- 四、DMA接收示例
- 五. 使用HAL库自带的HAL_UARTEx_ReceiveToIdle_DMA()函数实现DMA+空闲中断(相当多坑)
-
- 1.具体实现上与只使用DMA接收的区别
- 2.普通模式(不感兴趣的可以忽略)
- 3.循环模式
-
- 实现思路
- 使用STM32Cube FW_F4 V1.28.2 之前版本的请仔细阅读以下内容(新版本请忽略)!!!
-
- ***注意:下面的做法只有需要对数据分帧的才用关注,不然请忽略!!!***
- 总结说明
- 六、使用DMA进行串口发送
-
- 大概的工作流程
- 后话
前言
过往一直在用标准库进行开发,最近手头上有一批不用的板子,一看MCU是STM32F4系列的(以前玩的都还是STM32F1),莫名产生了“踏上新时代的船”的想法,直接下载STM32CubeIDE,使用HAL库开发来调通这些板子。我第一步调试的就是几乎不管什么应用场景都会涉及到的串口,又因为看到好几篇使用串口DMA的文章,决定使用以前没用过的DMA+空闲中断来进行串口接收数据。
第一次写博客,没想到来写前言时已经过万字了。之所以花那么多时间来表述这些学习成果,是因为学习过程中浏览到很多其他网友(其中不少是嵌入式初学者)调试DMA和空闲中断时遇到各种各样的问题,而他们在借鉴的文章中留言这些问题往往很难得到博主及时回应。希望不管是初学者还是有一定经验的开发者,都能从这篇文章中获得自己想要的东西。抛砖引玉,欢迎大家在评论区留言交流!
一、DMA是什么?
在传统的计算机系统中,外设设备需要通过CPU来控制数据的传输。当外设设备需要读取或写入数据时,需要向CPU发出请求,CPU则负责处理这些请求和数据传输的操作。这种方式会占用CPU的时间和资源,降低计算机系统的整体性能。
DMA控制器是一个特殊的硬件设备,它可以直接和系统内存进行数据传输,而不需要通过CPU来控制。在数据传输过程中,外设设备会向DMA控制器发送请求,告诉它需要读取或写入的数据的地址和大小。然后DMA控制器会直接从内存中读取或写入数据,完成数据传输的过程。这样就可以减少CPU的负担,提高数据传输的速度和效率。
二、相关配置
在HAL库的初始化中,DMA的全满中断默认使能,实际上半满中断也是默认使能的,感兴趣的可以看看函数
HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
的代码。
外设->内存 模式下,如果是直传模式,每当有数据被外设送入到FIFO,这些数据会直接从FIFO送到目标地址存储。如果是FIFO模式,FIFO中的数据会等待中断再被传输:
三、DMA工作模式
【1】直传模式(Direct Mode)
直传模式下的普通模式和循环模式,半满和全满中断都是按照用户配置的\"接收数据长度\"(也就是NDTR)正常产生。
【2】FIFO模式(FIFO Mode)
友情提醒
这部分的内容有点复杂,本人实际应用也没有使用上,只是学习时顺带研究调试了一下,本意只是大概搞明白和Direct Mode的基本区别,没想到疑惑一个接一个,很多地方用户手册也没说明白,最后居然记录了这么多!我将这部分的学习成果分享出来,希望其他使用FIFO Mode的朋友遇到问题了有个参考。不感兴趣的可以直接忽略这部分。
介绍
在两边数据流数据宽度不一致的情况下,需要设置FIFO Mode(或叫\"突发模式\")。
当数据宽度不一致,一方的单份数据可能是另一方的多份数据,若使用直传模式每次都从FIFO中搬运同样长度的数据,一定会出现数据过载或欠载的情况。因此突发模式通过在FIFO中控制输出单份数据的长度,避免数据流出错。
每个数据流配置有一个4字即16字节的FIFO,在突发模式下,通过配置溢出阈值、数据宽度、节拍数来设置每次在FIFO中搬运的数据长度。
- Burst Size (节拍数):每次在FIFO搬运数据的份数;
- Data Width (数据宽度):每份被搬运数据的字长;
- Threshold (FIFO阈值):FIFO产生中断所需达到的阈值(1/4满【4字节】、半满【8字节】、3/4满【12字节】、全满【16字节】)。
显而易见的是需要保证搬运数据时FIFO中的数据长度满足该次搬运的数据长度要求,否则数据流就可能出错,这意味着中断时FIFO中的数据长度需要是(Burst Size × Data Width)的倍数,不满足要求的配置是被禁止的:
ps:上图表格没有写出MBURST=single tranfer的情况(即每份数据都会被立即传输),因为这与直传模式一样,事实上直传模式下MBURST会由硬件强制置成single tranfer。
举个例子,若数据宽度设置为Byte,溢出阈值设置为3/4,那每当FIFO缓冲12字节数据就会产生中断请求搬运数据到目标地址:
- 若Burst Size设为4,那就是一次搬运(4 × 1Byte = 4Bytes),也就是一次中断会发生12/4 = 3份数据传输,是不会导致数据流出错的;
- 但若Burst Size设为8,12字节数据无法分成2份被搬运,因此这种配置是被禁止的。
下图非常好地描述了突发模式中FIFO对数据的存储和输送:
普通模式下
当NDTR变为0时或达到FIFO阈值时都会触发中断,半满中断和全满中断的标志位都有可能为1,下文会详细分析。无论哪种中断标志位组合都会导致禁止中断使能(非循环模式下,HAL库的中断处理函数中会把中断标志位为1的中断类型禁掉):
/* Half Transfer Complete Interrupt management ******************************/ if ((tmpisr & (DMA_FLAG_HTIF0_4 << hdma->StreamIndex)) != RESET) {...}/* Disable the half transfer interrupt if the DMA mode is not CIRCULAR */ else {...}
/* Transfer Complete Interrupt management ***********************************/ if ((tmpisr & (DMA_FLAG_TCIF0_4 << hdma->StreamIndex)) != RESET) {...} /* Disable the transfer complete interrupt if the DMA mode is not CIRCULAR */ else {...}
此时需要重置NDTR,否则不会再产生任何中断。由于HAL库的中断处理函数会自动清除标志位,重新使能中断;我们只需在回调函数中再次调用HAL_UART_Receive_DMA()
即可重置NDTR。
循环模式下
当NDTR变为0时不会触发中断,且NDTR会直接重置;由于处于循环模式,HAL库的中断处理函数不会禁止中断使能位。只有FIFO数据达到阈值时才可能会产生中断。
FIFO Mode具体分析
看到这里大家可能会有种违和感,FIFO Mode的中断理应是由每个Stream的FIFO配置的\"FIFO阈值\"决定的,而FIFO大小是固定16字节的,也就是说触发中断的阈值也是几个固定的数,这个过程中和用户自己设置的NDTR又有什么关联?
下面列几个FIFO Mode中循环模式下的实验现象,数据宽度均为Byte(黄色标注为发生中断):
【1】FIFO Threshold:3/4 ,NDTR:14,此时FIFO阈值 = 12 > 7 = 1/2 NDTR:
【2】FIFO Threshold:1/4 ,NDTR:6,此时FIFO阈值 = 4 > 3 = 1/2 NDTR:
【3】FIFO Threshold:1/4 ,NDTR:10,此时FIFO阈值 = 4 < 5 = 1/2 NDTR:
只看前两个实验现象的话,虽然确实触发中断了,但可能对两个标志位的值一头雾水;但对照第三个实验数据就一目了然了:实验三中接收数据长度为4和24时,即使长度达到FIFO阈值了却仍因为两个中断标志位都为0而无法触发中断。说明即使在FIFO Mode,两个中断标志位仍是由NDTR决定的!
FIFO Mode下,半满和全满标志位仍会因为NDTR变为初始值一半和0时“变化”,之所以带“引号”是因为标志位的值不会实时变化反映出来,即使到了半满和全满,Debug里看标志位也还是0。但当FIFO中数据达到FIFO阈值时,MCU会判断之前记录的半满和全满标志位是不是为1,如果有任意一个为1则触发中断。可以说,FIFO阈值是触发中断的第一条件,半满全满中断标志位是触发中断的第二条件。
四、DMA接收示例
下面以Direct Mode、循环模式下一个8字节大小的接收缓冲区示例,单片机串口接收完后打印缓冲区内的数据:
-
初始化后,NDTR=8:
-
接收到1字节数据后,NDTR=7:
-
再接收到2字节数据后,NDTR=5:
-
当缓冲区被写满后,即NDTR为0时会触发全满中断。普通模式下需手动清除标志位;循环模式下NDTR会自动重置(到0时自动重置回缓冲区长度),写指针回到缓冲区起始地址,且此时会触发中断回调函数:
-
再接收到数据时,将从缓冲区起始位置重新开始写入:
五. 使用HAL库自带的HAL_UARTEx_ReceiveToIdle_DMA()函数实现DMA+空闲中断(相当多坑)
DMA接收的好处不再赘述,缺点是DMA需要FIFO阈值达到设定值才能触发中断,如果接收的数据少于这个阈值,MCU就无法通知我们缓冲区的数据更新了。因此空闲中断就派上用场了:每当串口接收完一个字节数据后检测到下一个bit不为起始位,说明接收完连续的一帧数据了,就会产生空闲中断。
1.具体实现上与只使用DMA接收的区别
使用HAL库自带的HAL_UARTEx_ReceiveToIdle_DMA()
函数,当DMA触发了半满中断或全满中断时,同样是会进入函数到HAL_DMA_IRQHandler()
的 ,区别在于调用的回调函数不同了。以半满中断为例:
static void UART_DMARxHalfCplt(DMA_HandleTypeDef *hdma){/* --- 无关的已省略 --- *//* Check current reception Mode : If Reception till IDLE event has been selected : use Rx Event callback */ if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE) { /*Call legacy weak Rx Event callback*/ HAL_UARTEx_RxEventCallback(huart, huart->RxXferSize / 2U);} else { /*Call legacy weak Rx Half complete callback*/ HAL_UART_RxHalfCpltCallback(huart); }
可以看到,若串口接收模式被设置成空闲中断接收,回调函数会调用HAL_UARTEx_RxEventCallback()
而不是 HAL_UART_RxHalfCpltCallback()
,这也是为什么接收到数据后不是进入之前DMA接收设置的回调函数的原因。
结论:使用HAL_UARTEx_ReceiveToIdle_DMA()
,不管是半满中断、全满中断还是空闲中断,都是调用同一个回调函数HAL_UARTEx_RxEventCallback()
。用户需要重写该函数,三种中断的处理都在里面进行。
2.普通模式(不感兴趣的可以忽略)
接下来看一下为什么普通模式下,接收完一次数据DMA就不工作了:
在普通模式下触发空闲中断时,HAL库串口中断函数中会自动进行配置(已把不相关的代码省去):
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart){/* --- 无关的已省略 --- */ /* Check current reception Mode : If Reception till IDLE event has been selected : */ if ((huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE) && ((isrflags & USART_SR_IDLE) != 0U) && ((cr1its & USART_SR_IDLE) != 0U)) { __HAL_UART_CLEAR_IDLEFLAG(huart);/* Check if DMA mode is enabled in UART */if (HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR)){ /* DMA mode enabled */ uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx); if ((nb_remaining_rx_data > 0U) && (nb_remaining_rx_data < huart->RxXferSize)) { /* In Normal mode, end DMA xfer and HAL UART Rx process*/ if (huart->hdmarx->Init.Mode != DMA_CIRCULAR) {/* Disable the DMA transfer for the receiver request by resetting the DMAR bit in the UART CR3 register */ ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR); /* At end of Rx process, restore huart->RxState to Ready */ huart->ReceptionType = HAL_UART_RECEPTION_STANDARD; ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);/* Last bytes received, so no need as the abort is immediate */ (void)HAL_DMA_Abort(huart->hdmarx);} /*Call legacy weak Rx Event callback*/ HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount)); } } }}//end of function
需要关注的几个点:
- 首先判断RxEventType是否为
HAL_UART_RECEPTION_TOIDLE
,且处理函数会自动清空IDLE标志; - DMAR置0导致了禁止DMA Mode的接收;
- ReceptionType从
HAL_UART_RECEPTION_TOIDLE
变为了HAL_UART_RECEPTION_STANDARD
,这会导致下次接收无法进入该段处理程序; - IDLEIE置0导致了空闲中断的失效;
HAL_DMA_Abort()
禁止了DMA流。
这就是接收一次数据后不论DMA设置的Buffer有没有满(也就是NDTR不为0),DMA传输都会停止的原因:触发了串口空闲中断。前4点是从串口配置禁止,第5点是从DMA配置禁止。所以普通模式下使用空闲中断要留个心眼:空闲中断是会影响到DMA的配置的,此时DMA作用只是作为单次数据接收的缓冲区,每次接收完都会重置。
结论:普通模式下,使用HAL_UARTEx_ReceiveToIdle_DMA()
,一旦进入过空闲中断就只能再一次调用HAL_UARTEx_ReceiveToIdle_DMA()
来开启DMA和串口的对应配置。
3.循环模式
反过来说,循环模式下,空闲中断就和DMA的运作独立开来了。DMA配置的Memory可以看作一个环形缓冲区,里面的数据并不会因为空闲中断清空。当我们配置好后,就得到了一个会自动写入数据的环形缓冲区,接下来就是如何从里面读取数据了。
关于环形缓冲区的介绍可以看这篇文章:ring buffer,一篇文章讲透它,讲得非常通俗易懂。
实现思路
读数据的前提肯定是有新的数据写入。空闲中断会通知我们数据更新了,因此首当其冲的问题是如何获取新写入到缓冲区数据的长度。 循环模式跟普通模式不同,NDTR不会每次接收完都被重置,但传入HAL_UARTEx_RxEventCallback()
的参数Size
却是 (预设的缓冲区大小 - NDTR),也就是已用缓冲区大小,具体参考HAL_UART_IRQHandler()
下面的几行代码:
/* Check if DMA mode is enabled in UART */ if (HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR)) { /* DMA mode enabled */ uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx);//返回的值就是NDTR ...... huart->RxXferCount = nb_remaining_rx_data; ...... HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount)); }
因此普通模式下把Size
当作写入长度是没问题的,但循环模式下就需要我们进一步计算了。
既然把循环模式的数据存放看作环形缓冲区,那Size
不就基本等同于一个自动更新的写索引了吗?我们维护一个自定义写索引rear
,每次进入空闲中断都更新一次:
rear = (Size != sizeof(RxBuffer))?Size:0;//若Size等于缓冲区长度证明写满了,写指针要回到起点
很容易就能得出写入数据的长度:
- 若
Size
>rear
,写入数据长度 =Size
-rear
; - 若
Size
<=rear
,写入数据长度 =Size
+ (sizeof(RxBuffer)
-rear
)。
使用STM32Cube FW_F4 V1.28.2 之前版本的请仔细阅读以下内容(新版本请忽略)!!!
多谢评论区里@quote6的一同探讨和调试,我发现在25年4月发布的STM32Cube FW_F4 V1.28.2的固件代码中,数据刚好写满缓冲区的情况已经会同时触发全满中断和空闲中断,因此下述内容请根据自己使用的固件版本来作参考!(2025.8.4新增说明)
紧接而来就有一个坑了:看到这里大家可能会想到,把全满中断关掉,反正也用不上,开着还会进入中断回调,还可能影响到上面的运算逻辑。这个想法本身是没问题的,但实际上回看上面普通模式贴的代码就会发现一个问题:
/* DMA mode enabled */ uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx); if ((nb_remaining_rx_data > 0U) && (nb_remaining_rx_data < huart->RxXferSize)) { if (huart->hdmarx->Init.Mode != DMA_CIRCULAR) {......} /*Call legacy weak Rx Event callback*/ HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount)); }
调用回调函数的条件,普通模式和循环模式是一样的:NDTR>0,且NDTR < 预设的BufferSize 。循环模式下一旦NDTR变为0会立刻重置为初始预设值,也就是说如果写入的数据刚好写满缓冲区,进入HAL_UART_IRQHandler()
时是 NDTR == 预设的BufferSize,自然就不会调用回调函数了!
不过虽说不能关全满中断,但其实我们一般不用关心数据是哪次中断进来的,只需要每次中断时把缓冲区的数据读取出来并放到FIFO就好了。因此每次中断回调中我们都对数据处理,就不会出现Size
<=rear
的情况了(全满中断时rear
设成0)。
注意:下面的做法只有需要对数据分帧的才用关注,不然请忽略!!!
但利用空闲中断处理数据的好处就是能对数据进行分帧。如果想实现,就必须在全满中断中解决单次数据刚好写满缓冲区的情况。目前想到的解决思路如下:
- 进入中断回调时若
Size
==sizeof(RxBuffer)
(或huart->RxEventType
==HAL_UART_RXEVENT_TC
),证明触发的是全满中断,此时需要加入一定延时(不能直接用HAL_Delay()
,SysTick中断抢占优先级最低,在这里调用只会导致程序卡死),等待DMA传输一个单位数据的周期,然后进行如下判断:- 若
NDTR
==sizeof(RxBuffer)
,证明触发中断后没有再往缓冲区里写数据了(中断不会打断DMA传输),本次写入就是刚好写满缓冲区。运算和之前所述一样; - 若
NDTR
!=sizeof(RxBuffer)
,证明数据还没写完,我们只要等待空闲中断到来再处理就行了,因此直接return即可;
- 若
- 进入中断回调时若
Size
!=sizeof(RxBuffer)
,证明触发的是空闲中断,和之前所述同样处理。
ps:
- 写入的数据刚好填满缓冲区,触发的是全满中断而不是空闲中断;
- 关于触发了全满中断后判断DMA是否仍在写入数据,目前我只能想到加延时再判断,若大家有更好的办法请不吝赐教。
代码仅供参考:
#define UART_RXBUF_MAXSIZE (8)//串口环形缓冲区结构typedef struct{ uint16_t front; //写索引 uint16_t rear; //读索引 uint8_t buf[UART_RXBUF_MAXSIZE]; //缓冲区}uart_buf_t, * p_uart_buf_t;uart_buf_t rxCirBuf = {};//读取串口缓冲区int get_data_fromRxBuf(uint8_t* data,uint16_t size){p_uart_buf_t p = &rxCirBuf;if(size == 0 || size > sizeof(p->buf))return -1;if(p->front < p->rear){memcpy(data,p->buf + p->front,size);p->front += size;}else{uint16_t rest = sizeof(p->buf) - p->front;memcpy(data,p->buf + p->front,rest);memcpy(data+rest,p->buf,size - rest);p->front = size - rest;}return 1;}//接收中断回调函数void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef * huart, uint16_t Size){if(huart->Instance == USART1){uint16_t BUFF_SIZE = sizeof(rxCirBuf.buf);//判断是否为全满中断if(Size == BUFF_SIZE){delay_us(100);//这里记得要用非中断延时uint16_t NDTR = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx);if(NDTR != BUFF_SIZE)return;}//计算数据长度uint16_t size = (Size > rxCirBuf.rear)?\\(Size - rxCirBuf.rear):(Size + (BUFF_SIZE - rxCirBuf.rear));//写索引更新rxCirBuf.rear = (Size != BUFF_SIZE)?Size:0;/*TODO:*///测试用:发送接收到的数据uint8_t dSend[size];if(1 == get_data_fromRxBuf(dSend, size))HAL_UART_Transmit(&huart1, dSend, size,20); }}
上面代码的测试部分还包含了读取缓冲区的操作,本文不再展开。
总结说明
上面所述的是如何使用循环模式下的环形缓冲区,包括应该在哪里、在什么时候处理接收的数据,并且如何获取到接收到数据的长度。每个人的应用场景不同,有可能处理的数据每帧都有包头包尾,或每帧数据是定长,或数据中就包含长度信息,因此处理时并不一定要计算size
。本文只是介绍了一个实现思路,环形缓冲区也只是进行了最基本的实现,实质上还有很多可挖掘可完善的地方,比如这篇博客里提到的一种情况:处理一帧数据过久会出现的情况(@大文梅)。
六、使用DMA进行串口发送
来都来了,顺便简单讲一下HAL_UART_Transmit_DMA()
发送数据的流程。
首先接收数据和发送数据不是同一个DMA流,数据流的方向都不一样,如果发现无法发送数据不妨先检查下是否忘记配置串口Tx的DMA流。
大概的工作流程
- 在
HAL_UART_Transmit_DMA()
里,发送数据前会自动打开中断使能和DMA流使能并清空中断标志位,配置好后令串口控制寄存器的DMAT位置1(USART_CR3_DMAT
)使能DMA Mode; - 使能后DMA开始从Memory将数据写入到TDR,而当TDR的数据全部送到移位寄存器后,TXE Flag会置1,说明TDR空了,DMA就又送入下一个数据了。 特别说明一下,整个过程TXEIE位是disabled的,因此每次TXE Flag置1都不会触发中断。另外
HAL_UART_Transmit()
也是一样,只有HAL_UART_Transmit_IT()
会使能TXEIE来利用TXE中断发送数据。 这样大家应该可以理解这三者的区别了,包括阻塞发送和非阻塞发送也清晰了。别再写完HAL_UART_Transmit_IT(&huart1, rxBuf, Size)
或HAL_UART_Transmit_DMA(&huart1, rxBuf, Size)
后接一句memset(rxBuf, 0, Size)
然后问为什么发不出数据或者少数据了; - 当全部数据数传输完成后,NDTR计数也到0了,触发了全满中断从而进入了
HAL_DMA_IRQHandler()
。HAL库会自动清空中断标志位,若非循环模式还会禁止DMA中断。最后调用库内置的回调函数UART_DMATransmitCplt()
; UART_DMATransmitCplt()
中会将DMAT置0,禁止DMA Mode;然后USART_CR1_TCIE
置1使能传输完成中断。最后调用回调函数HAL_UART_TxCpltCallback()
,用户可以重写该函数来实现自己的需求。
后话
可能很多人会想:你这文章也太啰嗦了,我都用HAL库了还要去刨它库函数怎么运行吗?直接写解决方案就行了,顺便把源码附上()。
我的想法是:如果你能一次把功能调通且往后也不会出问题,那确实是可以。但是随着应用场景的变化,外设的变化甚至是MCU的变化,你能保证你的代码同样适用吗?嵌入式开发本身就是不断修正的过程。出问题不是问题,出了问题不会找原因才是问题。 而我文章内容就是一个不断出问题、找原因、解决问题的过程。
举个例子,网上其它使用DMA+空闲中断的教程,都是简单地教你使用HAL_UARTEx_ReceiveToIdle_DMA()
,普通模式下要在接收完一次数据后再调用一次。但我在学习过DMA后,第一想法是只调用这个函数一次,因为DMA开启之后没必要每次都去重置,尤其是缓冲区还有很多剩余空间的情况下。我的想法是串口中断应该只会禁止串口的DMA相关使能位,我只要在中断中重新使能就好了。但写了代码之后发现不行,debug时发现整个DMA流都被禁掉了。一看原来HAL库的串口中断函数里调用了HAL_DMA_Abort(huart->hdmarx)
,空闲中断发生就自动给你把DMA流禁了。虽然我在小标题标明了“不感兴趣的可以忽略”,但其实我相信总有人像我一样感兴趣的~
HAL库开发确实大大简化了开发过程,也降低了开发门槛,但这不代表我们对自己的要求降低了。同样的坑,你过了门槛踩到它时都是一样深的,爬出来也是一样费劲。你是想等有人路过把你拉出来,还是自己慢慢努力爬出来?