注意事項
最近發現使用HAL庫來操作UART,容易通訊線路上信號干擾或其它原因(比如用錯誤的波特率)進入ERROR狀態,一旦進入ERROR狀態在不做處理的情況下串口將無法正常工作,下面的程序均沒有說明進入ERROR狀態後該進行如何處理。
目的
在前面文章 《使用HAL庫開發STM32:UART基礎使用》 中介紹的UART的基礎使用,基礎使用非常簡單,不過在實際應用過程中僅基礎方法可能不是那麼方便,還需要編寫更多代碼來完善使用。這篇文章將對常見的數據發送接收處理方式做個演示。
注1:在STM32開發時因爲默認分配的堆內存不大,我個人比起使用malloc或是new方法申請內存,更多的喜歡把數據放在靜態區域;(這樣編譯的時候也可以看到內存佔用情況)
注2:本文中有些功能使用C++作爲演示,實際使用中也可以自行改爲純C代碼實現;
發送處理
存在的問題
前面文章中講到我們通常使用非阻塞方式來收發數據,這裏就產生了一個問題,如下代碼:
void fun(void)
{
uint8_t data[256] = {0};
// TODO
HAL_UART_Transmit_DMA(&huart1, data, 256); //將data數組內容通過UART發送
}
int main(void)
{
Init();
fun();
while (1)
{
}
}
上面代碼中fun函數裏聲明瞭一個數組,然後通過UART以非阻塞的方式進行發送,在調用發送函數後緊接着會立即退出fun函數,dara數組內存會被釋放,但這個時候發送還在進行,這裏就有可能發生發生數據不對或是程序跑飛等問題。
此外還有一個問題是同一個串口如果以非阻塞方式發送數據,在數據還未發送完的時候再次調用發送函數就會出錯。
解決方法
對於第一個問題解決方法很簡單,把data聲明放到外面就成:
uint8_t data[256] = {0};
void fun(void)
{
// TODO
HAL_UART_Transmit_DMA(&huart1, data, 256); //將data數組內容通過UART發送
}
或者用動態申請的方式:
uint8_t *data;
void fun(void)
{
data = (uint8_t*)malloc(256); //申請內存
// TODO
HAL_UART_Transmit_DMA(&huart1, data, 256);
}
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart == &huart1)
{
free(data); //發送完成後釋放內存
}
}
對於第二個問題解決方法也不麻煩,通過觀察可以知道HAL庫的串口發送函數傳入參數除了串口對象以外還有數據地址和長度,只要把數據地址和長度保存到下來,然後一個一發送即可,可以參考下節。
個人常用處理方式
下面是我個人對於串口發送常用的處理方式:
lib_fakeheap代碼如下:
#ifndef LIB_FAKEHEAP_H_
#define LIB_FAKEHEAP_H_
#include "main.h"
class LibFakeHeap {
public:
LibFakeHeap(uint8_t *buf, size_t size);
~LibFakeHeap(void);
uint8_t *get(size_t size);
private:
uint8_t *_buf;
size_t _size;
size_t _index;
};
#endif /* LIB_FAKEHEAP_H_ */
#include "lib_fakeheap.h"
LibFakeHeap::LibFakeHeap(uint8_t *buf, size_t size) :
_buf(buf), _size(size), _index(0) {
}
LibFakeHeap::~LibFakeHeap(void) {
}
uint8_t *LibFakeHeap::get(size_t size) {
if ((size == 0) || (size > _size)) {
return nullptr;
}
if ((_index + size) > _size) {
_index = size;
return _buf;
}
uint8_t *tmp = _buf + _index;
_index = (_index + size) % _size;
return tmp;
}
代碼非常簡單,功能上就是一開始聲明個大點的靜態數組,然後使用的時候動態分配。
這個方式和malloc或是new差不多,好處是用完不用釋放,缺點是所佔用的內存無法它用。另外這個代碼使用是基於一個前提的——單位時間內需要發送的數據最大數量是能預估的。
在使用時需要根據業務功能來估計聲明的靜態數組的大小,最好是單位時間內最大需求的兩倍。
lib_uart發送部分代碼如下:
#ifndef LIB_UART_H_
#define LIB_UART_H_
#include "main.h"
typedef struct {
uint8_t *data;
uint16_t size;
} LibUartTxInfo;
class LibUartTx {
public:
LibUartTx(UART_HandleTypeDef *uart, LibUartTxInfo *queue, size_t queuesize);
~LibUartTx(void);
bool write(uint8_t *data, uint16_t size);
void dmaTcHandle(UART_HandleTypeDef *uart);
private:
UART_HandleTypeDef *_uart;
LibUartTxInfo *_queue;
size_t _queuesize;
size_t _queuefront;
size_t _queuerear;
bool _sending;
};
#endif /* LIB_UART_H_ */
#include "lib_uart.h"
LibUartTx::LibUartTx(UART_HandleTypeDef *uart, LibUartTxInfo *queue, size_t queuesize) :
_uart(uart), _queue(queue), _queuesize(queuesize), _queuefront(0), _queuerear(0), _sending(false) {
}
LibUartTx::~LibUartTx(void) {
}
bool LibUartTx::write(uint8_t *data, uint16_t size) {
if ((_queuerear + 1) % _queuesize == _queuefront) {
return false;
}
_queue[_queuerear].data = data;
_queue[_queuerear].size = size;
_queuerear = (_queuerear + 1) % _queuesize;
if (!_sending) {
_sending = true;
HAL_UART_Transmit_DMA(_uart, _queue[_queuefront].data, _queue[_queuefront].size);
_queuefront = (_queuefront + 1) % _queuesize;
}
return true;
}
void LibUartTx::dmaTcHandle(UART_HandleTypeDef *uart) {
if (uart != _uart) {
return;
}
if (_queuerear == _queuefront) {
_sending = false;
return;
}
HAL_UART_Transmit_DMA(_uart, _queue[_queuefront].data, _queue[_queuefront].size);
_queuefront = (_queuefront + 1) % _queuesize;
}
上面代碼思路其實就是把待發送數據的地址和長度放到一個隊列裏,當沒有進行發送或發送完成時判斷下隊列內容,如果隊列不爲空則再次啓動發送。
數據接收與解析
和發送相比UART接收到真正使用更加麻煩點,因爲接收的時候會有更多不確定性,數據長度不定、數據傳輸出錯等等各種問題。一般的串口通訊中會制定一些帶有校驗功能的協議,只有接收到符合協議的數據才進行響應。一般的來說數據接收可以按下面方式處理:
數據接收
下面是數據接收的演示:
上圖中串口配置了中斷和DMA功能,其中DMA接收部分用了循環接收方式 。在 stm32f4xx_it.cpp
文件的 void USART1_IRQHandler(void)
函數中添加了空閒中斷相關處理。上圖中每次串口接收完成數據後會觸發空閒中斷,在空閒中斷中調用 fun
函數把 uartrxbuf
當前的數據發回上位機。在這裏的 fun
函數其實就是下文的數據解析函數,只不過這裏沒有進行解析而已。
上圖演示中用的是循環接收,但後面的演示中用的是普通接收方式,因爲HAL庫和循環接收的思路邏輯有點衝突。下面的代碼是上面演示中用到的代碼,實際使用中因爲邏輯上的衝突後面有些改動,最終代碼可以參考本文後邊給出的鏈接。
lib_uart接收部分代碼如下:
#ifndef LIB_UART_H_
#define LIB_UART_H_
#include "main.h"
class LibUartRx {
public:
LibUartRx(UART_HandleTypeDef *uart, DMA_HandleTypeDef *dma, uint8_t *buf, size_t bufsize, void (*dataParse)(size_t rear));
~LibUartRx(void);
void listen(void);
void uartIdleHandle(void);
private:
UART_HandleTypeDef *_uart;
DMA_HandleTypeDef *_dma;
uint8_t *_buf;
size_t _bufsize;
void (*_dataParse)(size_t rear);
};
#endif /* LIB_UART_H_ */
#include "lib_uart.h"
LibUartRx::LibUartRx(UART_HandleTypeDef *uart, DMA_HandleTypeDef *dma, uint8_t *buf, size_t bufsize, void (*dataParse)(size_t rear)) :
_uart(uart), _dma(dma), _buf(buf), _bufsize(bufsize), _dataParse(dataParse) {
}
LibUartRx::~LibUartRx(void) {
}
void LibUartRx::listen(void) {
__HAL_UART_CLEAR_IDLEFLAG(_uart);
__HAL_UART_ENABLE_IT(_uart, UART_IT_IDLE);
HAL_UART_Receive_DMA(_uart, _buf, _bufsize);
}
void LibUartRx::uartIdleHandle(void) {
if (__HAL_UART_GET_FLAG(_uart, UART_FLAG_IDLE)) {
__HAL_UART_CLEAR_IDLEFLAG(_uart);
_dataParse(_bufsize - __HAL_DMA_GET_COUNTER(_dma));
}
}
數據解析
數據解析需要根據具體業務進行,比如拿常見的Modbus-Rtu協議說明:
編寫相應的解析函數來執行操作,先看下面演示:
上面演示中註冊了兩條指令,mcu在收到相應指令後進行了應答,如果收到無法解析爲指令的數據就會濾過(演示中忘記示範了)。
上面的庫代碼和例程演示可以在我的GitHub項目中找到:
https://github.com/NaisuXu/STM32-tool-library-based-on-HAL-and-LL
對於HAL庫的吐槽
HAL庫設計了一套模式,讓用戶可以用上各個功能,但同時也帶來了一些問題。比如你只能按着它的思路來使用,不然就會可能出現各種問題,下面就是串口使用中出現的一些問題:
- 一般情況下串口接收用DMA循環接收是非常好的一種方式,但在HAL庫下就不太好用,只要一出現異常它就把串口和DMA全部關了(比如你用不匹配的波特率給串口發數據就必定出現幀錯誤),這在以前的STD庫裏面是不會有這樣的情況的(錯了就錯了出現問題時的幾個異常數據並不是啥大問題,反正接到的數據還要解析處理的,HAL庫倒好乾脆串口都給你關了,反應過度)。HAL庫的這種設計模式有時候反而把簡單的問題弄複雜了。
- 第二的我遇到的問題是我使用空閒中斷加DMA接收數據,在空閒中斷中處理數據後重啓串口DMA接收等待下次數據處理。就這點操作,邏輯上問題不大,在STM32F070F6P6上運行毫無問題,但同樣的代碼在STM32F405RGT6上就不正常工作了,空閒中斷中 重啓串口DMA接收 這個操作經常失敗,這就比較尷尬了,想好好用還得多處理一下。
總結
串口是蠻常用的功能,爲了使使用時更順手花時間整點工具還是值得的。這篇文章主要是提供了一種思路,上面代碼中也還有很多可以調整優化的地方。