目的
在前面文章 《使用HAL库开发STM32(基于F4):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
函数其实就是下文的数据解析函数,只不过这里没有进行解析而已。
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));
}
}
数据解析
数据解析需要根据具体业务进行,比如我有如下通讯协议:
编写相应的解析函数来执行操作,先看下面演示:
上面演示中注册了两条指令,mcu在收到相应指令后进行了应答,如果收到无法解析为指令的数据就会滤过(演示中忘记示范了)。
lib_naisu_protocol部分代码如下:
#ifndef LIB_NAISU_PROTOCOL_H_
#define LIB_NAISU_PROTOCOL_H_
#include "main.h"
/*
format:
STX CMD ADDR LEN-H(data) LEN-L(data) DATA(if have) BCC
*/
#define LIB_NAISU_PROTOCOL_MAX_SIZE 16
typedef struct {
void (*callback)(uint16_t size, uint8_t addr);
uint8_t stx;
uint8_t cmd;
} LibNaisuProtocolInfo;
class LibNaisuProtocol {
public:
LibNaisuProtocol(uint8_t *buf, uint16_t bufsize);
~LibNaisuProtocol(void);
bool add(void (*callback)(uint16_t size, uint8_t addr), uint8_t stx, uint8_t cmd);
void parse(size_t rear);
uint8_t read(uint16_t offset);
bool read(uint8_t *dest, uint16_t offset, uint16_t size);
uint8_t calBcc(uint8_t *data, uint16_t size);
private:
LibNaisuProtocolInfo info[LIB_NAISU_PROTOCOL_MAX_SIZE];
size_t _attachsize;
uint8_t *_buf;
uint16_t _bufsize;
uint16_t _front;
uint16_t _rear;
uint16_t _size;
uint16_t _cmdsize;
};
#endif /* LIB_NAISU_PROTOCOL_H_ */
#include "lib_naisu_protocol.h"
LibNaisuProtocol::LibNaisuProtocol(uint8_t *buf, uint16_t bufsize) :
_attachsize(0), _buf(buf), _bufsize(bufsize), _front(0), _rear(0), _size(0) {
}
LibNaisuProtocol::~LibNaisuProtocol(void) {
}
bool LibNaisuProtocol::add(void (*callback)(uint16_t size, uint8_t addr), uint8_t stx, uint8_t cmd) {
if (_attachsize >= LIB_NAISU_PROTOCOL_MAX_SIZE) {
return false;
}
info[_attachsize].callback = callback;
info[_attachsize].stx = stx;
info[_attachsize].cmd = cmd;
_attachsize++;
return true;
}
void LibNaisuProtocol::parse(size_t rear) {
_rear = rear;
_size = (_rear + _bufsize - _front) % _bufsize;
loop: if (_size < 6) {
return;
}
for (size_t i = 0; i < _attachsize; i++) {
if ((_buf[_front] == info[i].stx) && (_buf[(_front + 1) % _bufsize] == info[i].cmd)) {
uint16_t cmdsize = ((_buf[(_front + 3) % _bufsize] << 8) | _buf[(_front + 4) % _bufsize]) + 6;
if (cmdsize <= _size) {
uint8_t bcc = 0;
if ((_front + cmdsize - 1) <= _bufsize) {
bcc = calBcc(&_buf[_front], cmdsize - 1);
} else {
bcc = calBcc(&_buf[_front], _bufsize - _front);
bcc = bcc ^ calBcc(_buf, cmdsize - 1 - (_bufsize - _front));
}
if (_buf[(_front + cmdsize - 1) % _bufsize] == bcc) {
_cmdsize = cmdsize;
info[i].callback(_cmdsize, _buf[(_front + 2) % _bufsize]);
_cmdsize = 0;
_front = (_front + cmdsize) % _bufsize;
_size -= cmdsize;
goto loop;
} else {
_front = (_front + 1) % _bufsize;
_size--;
goto loop;
}
} else if (cmdsize < _bufsize) {
return;
} else { // cmdsize >= _bufsize
_front = (_front + 1) % _bufsize;
_size--;
goto loop;
}
}
}
_front = (_front + 1) % _bufsize;
_size--;
goto loop;
}
uint8_t LibNaisuProtocol::read(uint16_t offset) {
if (offset >= _cmdsize) {
return 0;
}
return _buf[(_front + offset) % _bufsize];
}
bool LibNaisuProtocol::read(uint8_t *dest, uint16_t offset, uint16_t size) {
if ((offset + size) > _cmdsize) {
return false;
}
uint16_t front = (_front + offset) % _bufsize;
if ((front + size) <= _bufsize) {
uint8_t *tmp = &_buf[front];
for (uint16_t i = 0; i < size; i++) {
dest[i] = tmp[i];
}
} else {
uint8_t *tmp = &_buf[front];
uint16_t hfsize = _bufsize - front;
for (uint16_t i = 0; i < hfsize; i++) {
dest[i] = tmp[i];
}
tmp = &dest[hfsize];
hfsize = size - hfsize;
for (uint16_t i = 0; i < hfsize; i++) {
tmp[i] = _buf[i];
}
}
return true;
}
uint8_t LibNaisuProtocol::calBcc(uint8_t *data, uint16_t size) {
uint8_t bcc = data[0];
for (uint16_t i = 1; i < size; i++) {
bcc = bcc ^ data[i];
}
return bcc;
}
总结
串口是蛮常用的功能,为了使使用时更顺手花时间整点工具还是值得的。这篇文章主要是提供了一种思路,上面代码中也还有很多可以调整优化的地方。
完整的包含上文中数据发送与接收解析的例程可以从下面链接下载:
https://download.csdn.net/download/Naisu_kun/12043656