Arm-CortexM0串口(下位机部分)
属性与原理
- 异步通信时不需要SCLK,同步通信时需要SCLK
- 串口接收和发送的过程:串口的接收和发送都是需要通过移位寄存器的
- 接收
- 一位一位的写入移位寄存器(也就是串转并)
- 写满之后发送——输出数据缓存区
- 无DMA时直接给内核(触发接收缓存laa满中断)
- 有DMA时直接写入到给定的内存地址
- 发送
- 先从内核到输出数据寄存器
- 读入到移位寄存器
- 按照波特率的节奏一位一位发出去
- 接收
- 通信前需要定义的5个内容
- 起始位
- 数据位
- 奇偶校验位——该位用来做校验,如果校验通过,接收的这一帧数据就被保留下来;如果校验失败,这一帧数据就被丢弃
- 停止位(一般是1、1.5、2个通信周期)
- 波特率
- 232最大有效传输20m,485可达3000米
不使用DMA时的串口通信过程:
- 对于发送:
- 发送缓存器空标志位会一直置起(开不开中断使能该标志位都是1)
- 因此使能发送寄存器空中断之后,程序会立马进入串口发送中断中
- 装载第一帧数据、移位指针、
- 下一次中断装载并发送下一帧,
- 依次循环
- 对于接收:
- 接收缓存器满后,进入中断
- 在中断中快速把这个数据接收起来,
- 在低优先级中处理判断接收buffer是否满了
- 如果满了在低优先级中回调用户
注:由于串口的通信速度远低于CPU的运行速度,因此不用害怕在调用pendsv来不及
使用DMA的串口通信过程:
- 在配置完DMA通道、并使能后,
- 对于接收:接收缓存满之后会自动申请DMA事件,DMA会自动将这一帧数据搬运到指定位置,一般会触发
- 串口接收超时的串口中断
- DMA接收半程的DMA中断
- DMA接收全程的DMA中断
- 对于发送:发送缓存器空之后自动申请DMA事件,将指定位置的数据一帧一桢搬运到发送缓存器中,一般会触发
- DMA发送半程的DMA中断(可选择,一般不用)
- DMA发送全程的DMA中断
- 也可关闭DMA中断,配置一个寄存器,等所有数据发送完成之后进入串口发送中断。
- 对于接收:接收缓存满之后会自动申请DMA事件,DMA会自动将这一帧数据搬运到指定位置,一般会触发
注:每次接收完成后不用重置DMA,又重新从buffer的第一位或中间一位开始,用一个指针记录即可
注意事项
- 发送缓存器空中断标志的标志位一直都是1,因此在中断服务函数中不要判断该标志位是1就执行一些操作,尽量先判断是否为接收中断,否则为发送中断
- 配置DMA时,首先关闭DMA通道,等所有的DMA属性配置完成后再打开相应的通道使能(否则在上电时容易自动搬运乱码)
- 使用DMA接收时,自定义接收buffer大小,此时回调有三种,
- buffer半满DMA中断,中断后需要回调或处理接收数据
- buffer全满DMA中断,中断后需要回调或处理接收数据
- 串口接收超时(一般是255个波特时常,可配置),将最终超时数据回调
因此要注意:
- 如果发生超时中断时DMA的指针在前半buffer的首位或者后半buffer的首位,么说明接收数据已经进过DMA的半/满程回调了,此时什么都不做,不要再重复搬运数据了
- DMA全满且这一条数据刚好接收结束,在计算长度时不能直接用DMA的指针减去接收起始地址,因为DMA的指针已经指向了首地址,此时计算数据长度容易出错
C#上位机串口通信原理详解
上位机的数据接收过程:
- 读可用长度bytetoread
- 创建相应长度内存并sp.read
- 通过invoke回调
存在问题:
- 为接收到更短的数据recveivebytesthreshold 一般设置为1(接收到一个字节就触发received事件)
- 但这样每次进入received事件之后读到的数据都很少(几个字节),如果每次都这样回调去处理的话,会导致程序很卡
因此可以考虑使用ARM的DMA思想,设置一个接收buffer,等待一帧数据全部接收完后再统一回调(接收超时后),此时需要申请一个定时事件来判断超时
- 第一种方式是每触发received事件之后,就先关闭上一次申请的定时器,然后在数据读取完之后,再重新申请一个定时事件,但是这样每次申请和撤销定时事件开销太大
- 第二种方法:只申请一次定时事件,具体实现如下
注意事项
- received事件的处理函数内部对数据的处理应尽可能快,因为received函数是可以重入的
- serialport类默认的接收和发送缓存器的长度为4096字节(可软件配置),工作原理类似环形寄存器
上位机代码
///定义全局变量
public static int receivedbuffer = 1; ///在此处定义datareceived事件发生前内部输入缓冲区buffer(这里设置为1,一旦收到数据就进received事件)
public static USART_struct USART_cfg = new USART_struct();
//定义串口发送缓冲区 长度是10240字节
public static byte[] UsartSendByteBuff = new byte[1024];
public static int UsartSendByteOffset; ///发送缓存区的指针偏移量
public static int UsartSendByteCount; ///发送帧数计数器
//定义串口处理缓冲区 长度是10240字节
public static byte[] UsartReceByteBuff = new byte[1024];
public static int UsartReceByteBuff_len;
//public static int UartReceByteBuff_offset; //定义偏移量
public static int UartReceByteBuff_offset_begin = 0; //定义开始存储位置
//串口接收数据的临时缓存区
public static int UsartReceByteBuff_temp_len;
public static byte[] UsartReceByteBuff_temp;
//定时开始标志
public static bool TimerEnable = false;
public static bool UartReceByteBuff_offset_begin_avilable = true;
public static bool IsTimeOutData = false;
public static bool IsReceiving = true; //用来判断这一帧数据接收完了没
public static long CheckTime = 0;
//定义接收超时
public static int RecTimeOut = 10; //单位/ms
System.Timers.Timer DataRecTimer = new System.Timers.Timer(1); //设计一个每1ms触发的定时事件,用来查看当前时间
//这里最好是设为最小值1,因为timer类依赖于系统时钟,系统时钟分辨率可能大于
//1ms,所以实际每次的定时触发可能是5ms左右
//否则容易出现没有更新到IsReceiving,导致多帧数据在一次回调
SerialPort sp ;
/// <summary>
///发生接收事件之后的回调函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
/// 注意,这个函数是被多个线程(多个串口)调用的,不同的sender用来区分不同的port
/// 该函数不在主线程内,因此不能再该函数中直接访问界面控件,而是通过invoke或begininvoke来调用
/// 串口接收原理:
/// 1.串口的默认接收缓存为4096(可软件修改)
/// 2.threshold设置为1,收到数据后触发received事件,并进入该函数
/// 3.在该函数中使用DMA思想,设置自己的buffer与超时时间
/// 4.满buffer或超时之后通过invoke回调,处理接收数据
/// 如果使用while循环判断接收buffer满了没有,可能会出现该函数的多次重入问题
/// 因此选择不断地读,写到自定义的buffer中,满了之后一起回调
private void SpDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
IsReceiving = true; //开始接收了
//接收数据长度
UsartReceByteBuff_temp_len = sp.BytesToRead;
//申请对应的长度
UsartReceByteBuff_temp = new byte[UsartReceByteBuff_temp_len];
//读取所接收到的数据 使用字节读出 读出的数据存储到0开始的位置
sp.Read(UsartReceByteBuff_temp, 0, UsartReceByteBuff_temp_len);
Buffer.BlockCopy(UsartReceByteBuff_temp, 0, UsartReceByteBuff, UartReceByteBuff_offset_begin, UsartReceByteBuff_temp_len);
UartReceByteBuff_offset_begin += UsartReceByteBuff_temp_len;
IsReceiving = false; //接收完了,让定时器走起来
}
catch
{
AppendLineTestResult("串口硬件错误", false, true);
}
}
/// <summary>
/// 超时处理函数
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void LastRecDataCall()
{
//定时器停止计数
IsReceiving = true;
//这里就不用拷贝数据了直接进行回调处理
IsTimeOutData = true;
this.Invoke(new EventHandler(UART_receive_dealwith));
}
/// <summary>
/// 函数被串口接收中断调用 接收长度 UsartReceByteBuff_temp_len 存储在 UsartReceByteBuff_temp
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UART_receive_dealwith(object sender, EventArgs e)
{
byte[] temp;
try
{
//首先判断是否为超时数据,其次将数据快速复制到临时缓冲区处理
temp = new byte[UartReceByteBuff_offset_begin];
Buffer.BlockCopy(UsartReceByteBuff, 0, temp, 0, UartReceByteBuff_offset_begin);
UartReceByteBuff_offset_begin = 0;
//这里不解析数据了,只是把接收到的数据打印到界面显示区
TxtRecData.Text += "接收到的数据:\r\n";
string strReceived = "";
{
int curByteIndex = 0;
for (curByteIndex = 0; curByteIndex < temp.Length; curByteIndex++)
{
strReceived += UsartReceByteBuff[curByteIndex].ToString("X2") + " ";
}
}
strReceived += "\r\n";
if (TxtRecData.TextLength > 10000)
{
TxtRecData.Text = strReceived;
}
else
{
TxtRecData.Text += strReceived;
}
//获取焦点
TxtRecData.Focus();
//光标定位到文本最后
TxtRecData.Select(TxtRecData.TextLength, 0);
//滚动到光标处
TxtRecData.ScrollToCaret();
}
catch (Exception Err)
{
int j = UsartReceByteBuff_temp_len;
AppendLineTestResult(Err.Message + "数据接收错误@UART_receive_dealwith" + j.ToString("D3"), false, true);
}
}
总结
本文对上位机(C#实现)与M0内核下位机进行串口通信的原理和实现过程进行了描述,两部分的注意事项是实现过程中容易出现的问题,希望能帮助到大家。