这几年生成式AI高速发展,AI编程已经从少数人的新玩具,发展到成了真正的生产力工具,我的好多前后端开发的老同事都已经非常依赖AI编程了(都是公司发的cursor账号),甚至还有人说离开cursor都不怎么会写代码了,而嵌入式作为编程领域的最保守的一派,也是时候与时俱进了。
最近接触了一段时间AI编程,这里演示一下cursor的在单片机驱动移植上的应用。
本文只是展示了一个AI编程的可能性,并非编写Prompt(提示词)的教程,毕竟我只也是cursor的新玩家,对Prompt的编写经验不多,文章里的Prompt用法肯定还是存在很大改进空间的。如何更好的与cursor交互,需要读者自己在使用中慢慢领悟。
如果完全没接触过cursor,建议先找一些入门的,包括安装、注册的教程。

cursor简单介绍

Cursor 是一款 AI 原生代码编辑器,基于 VS Code 内核开发,集成 GPT-4o、Claude 等大模型,支持实时 AI 代码生成、调试、重构与解释,适配多语言开发,可本地部署模型,兼顾开发效率与数据安全。
相比 GitHub Copilot,Cursor 是 AI 原生 IDE,项目级索引与多模型切换更灵活,Composer 支持跨文件重构,Chat 可直接应用修改,本地部署兼顾安全,复杂代码生成与重构更高效。
在Cursor对话框的下拉框中可以选择如下类型:

模式 核心思想 工作方式 典型场景
Ask 快速问答与代码生成 基于当前代码或问题,提供解释或代码片段,不主动修改文件。 理解代码逻辑、获取概念解释、生成小段代码。
Plan 先规划,后执行 分析任务后生成详细修改计划,经用户批准后自动执行所有更改。 大规模重构、添加复杂功能等需预先评估的任务。
Agent 全自动执行 自主分析、规划并直接调用工具(编辑、运行命令)完成任务,可能自主修复错误。 实现多步骤需求、探索性开发、追求最大自动化。
Debug 系统化诊断运行时 Bug 提出假设,引导收集运行时数据(如日志),基于证据定位原因并提供修复方案。 诊断复杂、难以复现的运行时错误。

(上面都是豆包写的)
注册账号后是free版,好像可以试用问几个Prompt(提示词),要开发还是直接买独享账号或者找人代充比较好(独享账号有风险,昨天碰到了账号从pro掉回了free,还好客服退了部分的钱)

任务

当前接了一个交期非常紧张的项目,没办法像以前那样完全手动改代码。
因此这里使用cursor来移植一下单片机串口驱动,被移植的驱动的信息如下:
DMA 数据发送(带发送队列,发送可设置发送间隔) + 中断接收超时断帧(接收双缓冲),单片机为APM32F103。
这是我使用了很多年串口驱动,按照经验,因为使用的外设类型较多,因此移植到新单片机并进行调试最少需要花费半天时间
本次需要移植到PY32F071这款单片机中,并进行一些基本的测试以确保移植的模块工作正常。
可以看到都是一些小众芯片,网上的信息估计不多,根据此特点,建立工程的方式有两种:

  • 在PY32F071的固件库中建立工程,这样可以让cursor直接参考相关外设例子;
  • 在新建工程后,再将外部的PY32F071的固件库路径告诉cursor,让它参考。

本文使用前者,因为目的仅仅是为了移植和测试某个硬件的驱动,先不用集成到最终的工程里。

前期准备

首先解压好PY32F071的固件库,并通过cursor->file->open folder打开固件库,随之点击cursor->file->save workspace as…将workspace保存到固件库的根目录下。
PY32F071的固件库分为hal库和ll库,习惯使用ll库,所以先让corsor屏蔽hal库,编写如下的Prompt,并使用默认的Agent模式:

当前文件夹是py32f071的SDK,./porjects/Example_ll是ll库的外设相关例子,本次开发仅使用ll库。./Drivers文件夹是相关的库,屏蔽掉py32f071_hal为前缀的文件,这些都是hal库的文件。

cursor提示在仓库根目录添加 .cursorignore(需你本地创建),在一个文本里填入cursor提示的内容,并改名为.cursorignore

PY32F071 SDK - 本工程仅使用 LL 库,屏蔽 HAL 库文件
Drivers/PY32F071_HAL_Driver/Inc/py32f071_hal*.h
Drivers/PY32F071_HAL_Driver/Src/py32f071_hal*.c
Drivers/PY32F071_HAL_Driver/Inc/py32f0xx_hal.h

这样 Cursor 在索引和搜索时会忽略这些 HAL 文件,减少误用和干扰。

接着找到固件库中功能类似的例子,这里用的是USART_HyperTerminal_DMA_Init,让cursor将它复制一份:

目前准备使用使用MDK编写一个串口的demo,使用USART_HyperTerminal_DMA_Init这个工程为模板,在同一个父文件夹内复制出uart_dma_demo工程文件夹

观察到已经复制出了一个uart_dma_demo工程文件夹
注:一开始让他基于模板生成一个新工程,结果cursor将目录生成到了别的路径,路径修改后,发现MDK里的有些文件打不开了,看来它对.uvprojx文件的解析并没有那么安全,因此这里让他复制到同个级别的路径下。

移植与调试

首先让cursor找到被移植到工程,并进行分析:

找到目录D:\xx_project\XXX\code\XXX_v0.3_xxxxx\xxx,其中MDK工程文件在D:\xx_project\XXX\code\XXX_v0.3_xxxxx\XXX\application\project下,分析其中的hal_uart3模块的接口和实现,将hal_uart3模块及其配套的模块比如my_queue或者hal_tick等移植到uart_dma_demo内。

移植完成后,新增目录与文件:

uart_dma_demo/
├── code/
│ ├── my_lib/
│ │ ├── my_queue.c # 队列(仅保留 hal_uart3 用到的接口)
│ │ └── my_queue.h
│ └── hal/
│ ├── hal_conf_py32.h # PY32 适配(临界区、USART2/DMA 宏)
│ ├── hal_systick.c # 5ms 周期 tick
│ ├── hal_systick.h
│ ├── hal_uart3.c # 串口 HAL(DMA 发送 + 中断接收)
│ └── hal_uart3.h
├── Src/
│ ├── main.c # 使用 hal_uart3 的 demo 流程
│ └── py32f071_it.c # SysTick + USART2 中断
└── Inc/
└── main.h # 删除旧 APP_* 回调声明

接上串口工具,发现打印正常,但接收只能接收一个字节的数据,根据现象反问cursor:

开机的打印正常,但接收12个字节数据,只打印了一个字节,分析原因

cursor分析后,得出结论,并自动进行了修改:

hal_uart3_tick_hand() 应只在 SysTick 等固定周期(5ms)中断中调用,不能在 main 的轮询或空转循环中调用。

经过测试接收后回显正常。
接下来对hal_uart3_send()发送间隔参数和接收超过缓存大小的数据进行测试:

测试一下,使用hal_uart3_send连续发送多种数据,发送间隔分别设置为50,0,1000,30,接下来将hal_uart3的单个接收缓冲区改为10字节,回显功能放到while(1)内,注意回显时候不需要发送间隔,我准备使用上位机发送一个100字节以上的数据,以测试串口的接收功能是否正常。

修改后的main()内容如下:

int main(void)
{
  /* Configure Systemclock */
  APP_SystemClockConfig();

  hal_tick_init();
  BSP_LED_Init(LED_GREEN);
  hal_uart3_init();

  /* 测试 hal_uart3_send 发送间隔:50ms, 0ms, 1000ms, 30ms */
  hal_uart3_send((uint8_t*)"[50ms] MSG1\n", 12, 50);
  hal_uart3_send((uint8_t*)"[0ms] MSG2\n", 11, 0);
  hal_uart3_send((uint8_t*)"[1000ms] MSG3\n", 14, 1000);
  hal_uart3_send((uint8_t*)"[30ms] MSG4\n", 12, 30);
  hal_uart3_send((uint8_t*)"\nRecv buf=10B, echo in loop. Send 100+ bytes:\n", 45, 50);

  BSP_LED_On(LED_GREEN);

  while (1)
  {
    if (hal_uart3_is_recv())
    {
      uint8_t *pRxData;
      uint8_t rxLen;
      hal_uart3_recv_get(&pRxData, &rxLen);
      if (rxLen > 0)
      {
        hal_uart3_send(pRxData, rxLen, 0);  /* 回显,无发送间隔 */
      }
    }
    __WFI();
  }
}

下载程序后打印效果如下:

[12:11:12.407]收←◆[50ms] MSG1
[12:11:12.461]收←◆[0ms] MSG2
[1000ms] MSG3
[12:11:13.471]收←◆[30ms] MSG4
[12:11:13.510]收←◆
Recv buf=10B, echo in loop. Send 100+ bytes:

而在多次接收74字节的数据的时候都正常,98字节则偶发丢包,而发送超过194字节两次,都是只回显了144字节。让cursor分析原因:

测试中发送98字节几次,回显一般正常,偶尔缺少一部分。而发送超过194字节两次,都是只回显了144字节。分析可能存在的问题,不要改动代码。

结论如下:

原因 说明
单一 is_recved 多次 recv_complete 会互相覆盖,主循环只能处理“最后一次”完成,其余数据相当于丢失。
发送队列容量 5 高密度回显时队列可能满,导致部分回显被丢弃。
无接收队列 完成事件不能累积,只能“读一次、处理一次”,无法应对快速连续接收。

根本问题是:接收端没有队列,只能表示“最近一次完成”,在高吞吐下必然出现覆盖和丢包;发送队列容量有限,会进一步放大回显不足的现象。

在以前和当前的这些项目中,双缓存的接收设计已经够用,只要将HAL_UART3_RECV_BUFF_MAX根据项目的需求改大,例如从前面的10字节改为100字节就OK了。
移植完毕。

不足:

在原工程中,hal_uart3.c/h是用于串口3的,而这个py32的库里的USART_HyperTerminal_DMA_Init,相关硬件选用的是串口2,cursor直接根据例子,串口相关硬件沿用了串口2,导致模块名字和功能不一致。
另外APM32F103中的原来工程,串口接收使用了串口中断一个个的接收,而不是使用DMA中断接收,估计是那个单片机的dma资源冲突或者不够用了,这里还是改回dma接收吧。
以上的两个问题,也让cursor修正了,这里就不细说了。

如果没有增加说明:“不要改动代码”,cursor的agent模式会直接修改代码,最好搭配git使用,多用commit或者stash保存现场,以方便查看究竟改了什么还有恢复上一个版本。

附带一下生成的工程:

通过网盘分享的文件:uart_dma_demo.zip
链接: https://pan.baidu.com/s/1TWK1wWNHqM7wlvKGfTZ5UA?pwd=eguk 提取码: eguk

参考链接:
cursor不能跳转函数定义(C/C++工程):https://www.cnblogs.com/storybrooke/p/19477351

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐