使用HAL庫開發STM32:UART進階使用

注意事項

最近發現使用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接收 這個操作經常失敗,這就比較尷尬了,想好好用還得多處理一下。

總結

串口是蠻常用的功能,爲了使使用時更順手花時間整點工具還是值得的。這篇文章主要是提供了一種思路,上面代碼中也還有很多可以調整優化的地方。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章