机器人爱好者1991 发表于 2023-10-19 16:41

[测评nucleo开发板f413zh]第4篇串口调试

<div class='showpostmsg'> 本帖最后由 机器人爱好者1991 于 2023-10-19 16:40 编辑

<p>&nbsp;&nbsp;&nbsp;&nbsp;</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;单片机的串口在调试中非常重要。串口可以用于打印单片机的变量数值,方便排查问题。在调试中都是非常重要的工具,可以显著的提高调试效率。</p>

<p><strong>一、</strong><strong>引脚</strong><strong>分配。</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;串口用到了stm32重要的外设资源,uart。对于这个板子是没有串口转换芯片的,直接就是TX、RX的引脚。原理图的CN9标注USART的地方,4和6引脚,分别是PD6(RX)和PD5(TX)引脚。我们就选择这个。</p>

<p><strong>二、</strong><strong>串口</strong><strong>的概念</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;串口是一种通信接口,用于在设备之间传输数据。在STM32中,串口主要分为UART和USART两种类型。<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;UART是通用异步收发传输器(Universal Asynchronous Receiver Transmitter)的缩写,它是一种简单的串口通信方式。UART只支持异步通信,也就是数据的传输不需要时钟信号,而是通过起始位、数据位、校验位和停止位来进行数据的传输。<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;USART是通用同步异步收发传输器(Universal Synchronous/Asynchronous Receiver Transmitter)的缩写,它是一种更为复杂的串口通信方式。USART既支持异步通信,也支持同步通信。异步通信与UART相似,而同步通信则需要使用外部时钟信号来同步数据的传输。<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;总的来说,UART和USART都是串口通信的方式,UART是一种简单的异步通信方式,而USART则是一种更为复杂的通信方式,支持异步和同步通信。在选择使用哪种串口通信方式时,需要根据具体的应用需求和外部设备的要求来决定。</p>

<p><strong>三、</strong><strong>串口</strong><strong>的配置</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;配置相对简单,就是设置Mode为异步,打开串口2的全局中断,设置引脚,设置比特率等信息。</p>

<p><strong>四、</strong><strong>串口</strong><strong>的发送</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;代码主要用于在STM32微控制器上配置和管理USART2的UART通信。 MX_USART2_UART_Init 函数用于初始化USART2的通信参数,包括波特率、数据位、停止位等,然后通过调用HAL_UART_Init函数完成初始化。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;HAL_UART_MspInit 函数负责配置与USART2相关的GPIO引脚和中断,包括使能USART2时钟、配置GPIO引脚为复用模式、设置中断优先级等。在STM32微控制器中,一个GPIO引脚可以有多种不同的功能,比如作为普通数字输入/输出、串行通信接口(如USART、SPI、I2C)的数据线,或者其他功能。这种切换功能的能力称为引脚的复用功能。Alternate 字段是一个用来指定GPIO引脚复用功能的设置。在这里,GPIO_AF7_USART2 指定了将这些引脚配置为与USART2通信相关的功能。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;最后,HAL_UART_MspDeInit 函数用于反初始化USART2,包括禁用USART2时钟、释放GPIO引脚配置以及禁用中断。这些函数协同工作以确保USART2 UART通信正确初始化和反初始化。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;中断发送。while(huart2.gState != HAL_UART_STATE_READY);这是一个 while 循环,用于等待USART2的发送状态变为 HAL_UART_STATE_READY,即等待数据发送完成。在这个循环中,程序会不断检查 huart2.gState 的状态,如果不等于 HAL_UART_STATE_READY,就会一直循环等待。一旦 huart2.gState 变为 HAL_UART_STATE_READY,表示数据发送完成,循环结束,程序继续执行后续操作。</p>

<pre>
<code>HAL_UART_Transmit(&amp;huart2, (uint8_t*)send_buffer, strlen(send_buffer), HAL_MAX_DELAY);
while(huart2.gState != HAL_UART_STATE_READY);</code></pre>

<p>&nbsp;</p>

<p><strong>五、</strong><strong>串口</strong><strong>的接收</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;阻塞接收。定义一个数组 uint8_t RxBuffer = {0};直接在while(1)里面,设置接收的长度以及超时时间1s。跳出的条件是超时或者是接收完10个字节。</p>

<pre>
<code class="language-cpp">if(HAL_UART_Receive(&amp;huart1,RxBuffer,sizeof(RxBuffer),1000) == HAL_OK)
{
        HAL_UART_Transmit(&amp;huart1,RxBuffer,sizeof(RxBuffer),100);
}</code></pre>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 中断接收。在UI界面开启中断与设置中断函数。设置中断接收HAL_UART_Receive_IT(&amp;huart2, (uint8_t *)&amp;usart2_rx_buffer, 1);和中断处理函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)进行组合。</p>

<pre>
<code class="language-cpp">uint8_t RxBuffer = {0,1,2,3,4,5,6,7};
void MX_USART2_UART_Init(void)
{
   /* USER CODE BEGIN USART2_Init 2 */
    HAL_UART_Receive_IT(&amp;huart2, RxBuffer, sizeof(RxBuffer));
    /* USER CODE END USART2_Init 2 */
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart == &amp;huart2)
    {
      HAL_UART_Transmit(&amp;huart2, RxBuffer, sizeof(RxBuffer), 100);
      memset(RxBuffer, 0, sizeof(RxBuffer));
      HAL_UART_Receive_IT(&amp;huart2, RxBuffer, sizeof(RxBuffer));
    }
}</code></pre>

<p>&nbsp;</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;我这里出现的问题是大于sizeof(RxBuffer)之后,就不进入中断了。实际想接收8个字符/次,上位机发送9个字符之后,整个串口中断就进去不了了,缓冲区阻塞了。最后,将RxBuffer的size设置成了1,会好一些;又将波特率从115200变成9600就更好了。这个问题以后有时间再详细探究。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DMA接收。DMA的接收有时间再补,随便写可能会有遗漏。</p>

<p><strong>六、通信协议数据的发送接收</strong></p>

<p><strong>6.1 接收字符的处理</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;因为我发送的0x01的时间和0x02的速度,消息不定长,我们采用的是每次接收一个字符。然后进行拼接。不只是单片机,我的上位机也使用了一次一个字符进行拼接的策略。我定义了一个struct,struct里面是rx_buffer来存储接收缓冲;rx_index表示接收第几个字节了;frame_started类似于一个bool数值,表示帧接收完帧头否。</p>

<pre>
<code class="language-cpp">typedef struct
{
    uint8_t rx_buffer; // 接收缓冲
    uint8_t rx_index; // 接收缓冲区索引
    uint8_t frame_started; // 帧接收完帧头否
} rx_data_t;</code></pre>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;下面的接收流程,就是一个个的字符接收,然后拼成完整的一帧。主要功能是根据特定的帧头和帧尾标识,进行 CRC 校验,最后解析接收到的数据帧。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;这里面分为帧头的检测、帧尾的检测、CRC16-Modbus的校验,完整数据的输入解析。如果索引为0且接收到的字符等于帧头1(0x55),则重置接收,将接收索引设置为1,并将接收到的字符存入接收缓冲区。如果索引为1且接收到的字符等于帧头2(0xaa),则设置frame_started标志表示有新的数据帧开始,将接收索引设置为2,并将接收到的字符存入接收缓冲区。如果索引大于等于2且frame_started标志为真,则将接收到的字符存入接收缓冲区,并递增接收索引。判断是否接收到了帧尾。如果接收索引大于等于6且接收缓冲区倒数第二个字符等于帧尾1,并且倒数第一个字符等于帧尾2,则执行以下操作:调用函数计算接收缓冲区中数据部分的 CRC16 校验值,函数解析接收到的数据帧。最后,重置frame_started标志、接收索引和接收缓冲区,以准备接收下一帧数据。</p>

<pre>
<code class="language-cpp">void recv_uchar(const uint8_t *buffer, const int16_t length)
{   
    uint8_t crc_low, crc_high;
    rx_buffer_temp=buffer;
    // 1. 如果,索引为0而且收到等于帧头1的字符,就得重置接收
    if (rx_data.rx_index == 0 &amp;&amp; rx_buffer_temp == 0x55)
    {
      rx_data.rx_index = 1;
      rx_data.rx_buffer = rx_buffer_temp;
    }
    // 2. 如果,索引为1而且收到等于帧头2的字符,设置个 frame_started 来表示有新数据了
    else if (rx_data.rx_index == 1 &amp;&amp; rx_buffer_temp == 0xaa)
    {
      rx_data.rx_index = 2;
      rx_data.frame_started = 1;
      rx_data.rx_buffer = rx_buffer_temp;
    }
    // 3. 对数组以后的接收处理
    else if (rx_data.rx_index &gt;= 2 &amp;&amp; rx_data.frame_started)
    {
      rx_data.rx_buffer = rx_buffer_temp;
      // 4. 判断是否收到了帧尾
      if (rx_data.rx_index &gt;= 6 &amp;&amp; rx_data.rx_buffer == frame_footer1 &amp;&amp;
            rx_data.rx_buffer == frame_footer2)
      {
            // 5. crc16校验
            calculate_crc(&amp;rx_data.rx_buffer, rx_data.rx_buffer, &amp;crc_low, &amp;crc_high);
            if (crc_low == rx_data.rx_buffer &amp;&amp;
                crc_high == rx_data.rx_buffer)
            {
                // printf("CRC success\n");
                parse_serial_data(rx_data.rx_buffer, rx_data.rx_index);
            }
            else
            {
                printf("CRC failed\n");
            }
            rx_data.frame_started = 0;
            rx_data.rx_index = 0;
            memset(rx_data.rx_buffer,0,sizeof(rx_data.rx_buffer));
      }
    }
    else
    {
      ;
    }
}</code></pre>

<p>&nbsp;</p>

<p>&nbsp;</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;对于速度数据的处理,就是按照小端格式就行赋值即可。</p>

<pre>
<code class="language-cpp"> /*解析返回的速度*/
void recv_velocity_cmd(const uint8_t *buffer, const int16_t length, int16_t recv_velocity)
{
        if (0x06 != buffer || 0x02 != buffer)
        {
       return;
        }
        recv_velocity = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        recv_velocity = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        printf("left_velocity=%d,right_velocity=%d\n", recv_velocity, recv_velocity);
    send_velocity=recv_velocity;
    send_velocity=recv_velocity;
}
/*解析返回的时间*/
void recv_time_cmd(const uint8_t *buffer, const int16_t length, int16_t recv_beijing_time)
{
        if (0x06 != buffer || 0x01 != buffer)
        {
       return;
        }
        recv_beijing_time = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        recv_beijing_time = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        recv_beijing_time = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        recv_beijing_time = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        recv_beijing_time = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        recv_beijing_time = (int16_t *)(buffer + ((int16_t)(buffer) &lt;&lt; 8));
        printf("year=%d,month=%d,day=%d,hour=%d,minute=%d,second=%d\n", recv_beijing_time,recv_beijing_time,recv_beijing_time,recv_beijing_time,recv_beijing_time,recv_beijing_time);
}</code></pre>

<p><strong>6.2 字符命令的发送</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;按照0-1为帧头,0x06表示写入,0x01表示时间,下一个字节是数据的长度,下面的按照小端格式对年月日时分秒,进行每个变量2个字节的占位进行赋值。接线来进行CRC16的校验填充,最后加两个帧尾。</p>

<pre>
<code class="language-cpp">void send_current_time(const int16_t beijing_time)
{
        uint8_t write_data;
        uint8_t crc_low, crc_high;
        int16_t send_index = 0;

        write_data = frame_header1;
        write_data = frame_header2;
        write_data = 0x06; // 写入
        write_data = 0x01; // 时间
        write_data = 12;   // 数据长度

        // 将年、月、日、时、分、秒按小端格式存储到数组中
        write_data = beijing_time &amp; 0xFF;
        write_data = beijing_time &gt;&gt; 8;
        write_data = beijing_time &amp; 0xFF;
        write_data = beijing_time &gt;&gt; 8;
        write_data = beijing_time &amp; 0xFF;
        write_data = beijing_time &gt;&gt; 8;
        write_data = beijing_time &amp; 0xFF;
        write_data = beijing_time &gt;&gt; 8;
        write_data = beijing_time &amp; 0xFF;
        write_data = beijing_time &gt;&gt; 8;
        write_data = beijing_time &amp; 0xFF;
        write_data = beijing_time &gt;&gt; 8;

        // 计算CRC16校验
        calculate_crc(&amp;write_data, write_data, &amp;crc_low, &amp;crc_high);
        // 将CRC校验值赋值给write_data和write_data
        write_data = crc_low;
        write_data = crc_high;
        write_data = frame_footer1;
        write_data = frame_footer2;
        HAL_UART_Transmit(&amp;huart2,write_data,send_index,100);
}</code></pre>

<p>&nbsp;</p>

<p>&nbsp;</p>

<p>&nbsp;</p>

<p><strong>6.3 </strong><strong>串口</strong><strong>2的一些配置</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;当然得使能一下串口2的printf。</p>

<pre>
<code class="language-cpp">#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif /* __GNUC__ */

PUTCHAR_PROTOTYPE
{
// 将字符发送到UART2
HAL_UART_Transmit(&amp;huart2, (uint8_t *)&amp;ch, 1, HAL_MAX_DELAY);
return ch;
}</code></pre>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;串口2的初始化。</p>

<pre>
<code class="language-cpp">typedef struct
{
    uint8_t rx_buffer; // 接收缓冲
    uint8_t rx_index; // 接收缓冲区索引
    uint8_t frame_started; // 帧接收完帧头否
} rx_data_t;
rx_data_t rx_data; // 接收相关数据的结构体实例
uint8_t rx_buffer_temp = {0};


void MX_USART2_UART_Init(void)
{
    /* USER CODE BEGIN USART2_Init 2 */
    HAL_UART_Receive_IT(&amp;huart2, rx_buffer_temp, sizeof(rx_buffer_temp));
    rx_data.rx_index=0;
    rx_data.frame_started=0;
    memset(rx_data.rx_buffer,0,sizeof(rx_data.rx_buffer));
    /* USER CODE END USART2_Init 2 */
}</code></pre>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;串口2的单个字符接收函数。</p>

<pre>
<code class="language-cpp">// 接收数据回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    uint8_t crc_low, crc_high;
    if (huart == &amp;huart2)
    {
      rx_buffer_temp=huart-&gt;Instance-&gt;DR;
      // 1. 如果,索引为0而且收到等于帧头1的字符,就得重置接收
      if (rx_data.rx_index == 0 &amp;&amp; rx_buffer_temp == 0x55)
      {
            rx_data.rx_index = 1;
            rx_data.rx_buffer = rx_buffer_temp;
            // printf("recv header1\n");
      }
      // 2. 如果,索引为1而且收到等于帧头2的字符,设置个 frame_started 来表示有新数据了
      else if (rx_data.rx_index == 1 &amp;&amp; rx_buffer_temp == 0xaa)
      {
            rx_data.rx_index = 2;
            rx_data.frame_started = 1;
            rx_data.rx_buffer = rx_buffer_temp;
            // printf("recv header2\n");
      }
      // 3. 对数组以后的接收处理
      else if (rx_data.rx_index &gt;= 2 &amp;&amp; rx_data.frame_started)
      {
            rx_data.rx_buffer = rx_buffer_temp;
            // 4. 判断是否收到了帧尾
            if (rx_data.rx_index &gt;= 6 &amp;&amp; rx_data.rx_buffer == frame_footer1 &amp;&amp;
                rx_data.rx_buffer == frame_footer2)
            {
                // printf("recv footer\n");
                // HAL_UART_Transmit(&amp;huart2, rx_data.rx_buffer, rx_data.rx_index, HAL_MAX_DELAY);
                // 5. crc16校验
                calculate_crc(&amp;rx_data.rx_buffer, rx_data.rx_buffer, &amp;crc_low, &amp;crc_high);
                // HAL_UART_Transmit(&amp;huart2, &amp;rx_data.rx_buffer, rx_data.rx_buffer, HAL_MAX_DELAY);
                // HAL_UART_Transmit(&amp;huart2, crc_low, 1, HAL_MAX_DELAY);
                // HAL_UART_Transmit(&amp;huart2, crc_high, 1, HAL_MAX_DELAY);
                if (crc_low == rx_data.rx_buffer &amp;&amp;
                  crc_high == rx_data.rx_buffer)
                {
                  printf("CRC success\n");
                  ;
                       recv_data_parse(rx_data.rx_buffer, rx_data.rx_index);
                }
                else
                {
                  printf("CRC failed\n");
                  recv_data_parse(rx_data.rx_buffer, rx_data.rx_index);

                  // HAL_UART_Transmit(&amp;huart2, rx_data.rx_buffer, rx_data.rx_index, HAL_MAX_DELAY);
                  // HAL_UART_Transmit(&amp;huart2, &amp;rx_data.rx_buffer, rx_data.rx_buffer, HAL_MAX_DELAY);
                  // HAL_UART_Transmit(&amp;huart2, crc_low, 1, HAL_MAX_DELAY);
                  // HAL_UART_Transmit(&amp;huart2, crc_high, 1, HAL_MAX_DELAY);
                  // HAL_UART_Transmit(&amp;huart2, rx_data.rx_buffer, 1, HAL_MAX_DELAY);
                  // HAL_UART_Transmit(&amp;huart2, rx_data.rx_buffer, 1, HAL_MAX_DELAY);
                }
                rx_data.frame_started = 0;
                rx_data.rx_index = 0;
                memset(rx_data.rx_buffer,0,sizeof(rx_data.rx_buffer));

            }
      }
      else
      {
            ;
      }
      memset(rx_buffer_temp, 0, sizeof(rx_buffer_temp));
      HAL_UART_Receive_IT(&amp;huart2, rx_buffer_temp, sizeof(rx_buffer_temp));
    }
}</code></pre>

<p>&nbsp;</p>

<p><strong>6.3 测试</strong></p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;在windows上打开串口8,按下按键,会发送相应的指令到下位机。下位机将收到的指令recv数组赋值给send数组,发送到上位机,进行解析。</p>

<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;windows下的上位机,出现了一些汉字显示的问题,删除汉字,使用下面的指令编译了一下。</p>

<pre>
<code>C:\mingw64\bin\x86_64-w64-mingw32-gcc -o serial.exe serial_server.c -lpthread</code></pre>

<p>&nbsp;</p>

<p></p>

<p></p>

<p></p>
</div><script>                                        var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;"   style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
                                       
                                        if(parseInt(discuz_uid)==0){
                                                                                                (function($){
                                                        var postHeight = getTextHeight(400);
                                                        $(".showpostmsg").html($(".showpostmsg").html());
                                                        $(".showpostmsg").after(loginstr);
                                                        $(".showpostmsg").css({height:postHeight,overflow:"hidden"});
                                                })(jQuery);
                                        }                </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script>

机器人爱好者1991 发表于 2023-10-19 16:49

<div></div>
页: [1]
查看完整版本: [测评nucleo开发板f413zh]第4篇串口调试