矿石收音机论坛

 找回密码
 加入会员

QQ登录

只需一步,快速开始

搜索
查看: 556|回复: 5

[树莓派Pico][PIO][PWM]学习笔记:囫囵吞枣学习PIO

[复制链接]
     
发表于 2025-2-21 21:00:16 | 显示全部楼层 |阅读模式
今天又有时间了,从RP2040手册3.6.8章节照葫芦画瓢扒来一个PWM控制器。对它做了一些改装以适配Arduino环境。对汇编代码也改了一些,不用样例中的“写立即执行代码进状态机,装载PWM周期”的方法。而是直接从TX fifo推入周期,让状态机最开始两行代码读到周期后永久保存在ISR里。
代码如下:
  1. #include <Arduino.h>
  2. #include "pico/stdlib.h"
  3. #include "hardware/pio.h"
  4. #include "hardware/gpio.h"

  5. // PIO 程序机器码
  6. const uint16_t pio_program_instructions[] = {
  7.     0x80a0, //  0: pull   block                     
  8.     0x60c0, //  1: out    isr, 32                    
  9.     0x9080, //  2: pull   noblock         side 0     
  10.     0xa027, //  3: mov    x, osr                     
  11.     0xa046, //  4: mov    y, isr                     
  12.     0x00a7, //  5: jmp    x != y, 7                  
  13.     0x1808, //  6: jmp    8               side 1     
  14.     0xa042, //  7: nop                              
  15.     0x0085, //  8: jmp    y--, 5                     
  16.     0x0002, //  9: jmp    2                          
  17. };

  18. // 定义 PIO 程序结构体
  19. const struct pio_program pio_program = {
  20.     .instructions = pio_program_instructions,
  21.     .length = 10, // 指令数量
  22.     .origin = 0, // 分配起始地址
  23. };

  24. PIO pio = pio0; // 使用 PIO 0
  25. uint sm;
  26. uint pin = 2;
  27. uint period = 1000;    //周期
  28. uint level = 0;        //脉宽

  29. void setup() {
  30.   //启用串口
  31.   Serial.begin(115200);delay(2000);
  32.   Serial.println("I AM BEGINNING!");
  33.   // 初始化 GPIO 2 为 PIO 功能
  34.   gpio_init(pin);
  35.   pio_gpio_init(pio, pin);
  36.   //gpio_set_function(pin, GPIO_FUNC_PIO0);

  37.   // 初始化 PIO
  38.   uint offset = pio_add_program(pio, &pio_program);                // 加载 PIO 程序
  39.   Serial.printf("Loaded program at %d\n", offset);
  40.   sm = pio_claim_unused_sm(pio, true);                             // 分配一个状态机
  41.   pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
  42.   pio_sm_config c = pio_get_default_sm_config();                   //获取一个默认状态机配置
  43.   sm_config_set_wrap(&c, offset, offset + pio_program.length - 1); //设置PIO代码循环范围
  44.   sm_config_set_sideset(&c, 2, true, false);                       //偷窃delay域2位做Sideset
  45.   sm_config_set_sideset_pins(&c, pin);
  46.   sm_config_set_clkdiv(&c, 1); // 设置时钟分频(200 MHz / 1 = 200 MHz)
  47.   pio_sm_init(pio, sm, offset, &c);
  48.   pio_sm_set_enabled(pio, sm, true); // 启用状态机
  49.   //推入周期设置
  50.   pio_sm_put_blocking(pio, sm, period);
  51.   Serial.printf("Put period = %d\n", period);

  52.   //不断更改脉宽
  53.   while (true) {
  54.     Serial.printf("Level = %d\n", level);
  55.     pio_sm_put_blocking(pio, sm, level);
  56.     level = (level + 20) % 500;
  57.     delay(100);
  58.   }

  59. //  pio_sm_put_blocking(pio, sm, 200);  //for debugging

  60. }

  61. void loop() {
  62. }
复制代码



sm_config_set_sideset(&c, 2, true, false);
这一行折腾了很久。原因是(手册给出的)样例程序中并不包含这一句。但是因为不明原因,在arduino环境下是必须设置这一行的。或许在github源头里是有的。另外这个函数的参数2,true,false也是需要倒腾一阵才搞定。这些参数是和汇编代码中的“.side_set 1 opt”有呼应关系的,刚开始没注意到。

还有一些不明白的地方,但是毕竟囫囵吞枣地让它转起来了。这就先凑合着吧。
例如,
pio_program.origin中的origin,
uint offset = pio_add_program(pio, &pio_program)中的offset,
汇编中的jmp指令,
这三者之间的关系我就不是很清楚。问了deepseek很多次,绕着圈地问,都没得到很准确的回答。
origin是指定汇编程序加载位置的,设为-1,就会由系统自动指定加载位置。我的程序中,会加载到offset=22那个地址。考虑到机器码是10条指令,那么看来系统倾向于把指令加载到32字指令空间的末尾。
但是,jmp指令可是在绝对地址里跳的。这么乱加载程序,应该会导致程序跳错地址。
然而实践中并不会跳错。
因为没搞懂,所以我指定了origin=0。加载后也会返回offset=0。

最后给出机器码结构体的汇编源文件及其注释:
  1. .program pwm
  2. .side_set 1 opt
  3. pull block              ;Pull the period from FIFO to OSR then to ISR
  4. out isr,32
  5. period:
  6. pull noblock    side 0 ; Pull from FIFO to OSR if available, else copy X to OSR.
  7. mov x, osr             ; Copy most-recently-pulled value back to scratch X
  8. mov y, isr             ; ISR contains PWM period. Y used as counter.
  9. countloop:
  10. jmp x!=y noset         ; Set pin high if X == Y, keep the two paths length matched
  11. jmp skip        side 1
  12. noset:
  13. nop                    ; Single dummy cycle to keep the two paths the same length
  14. skip:
  15. jmp y-- countloop      ; Loop until Y hits 0, then pull a fresh PWM value from FIFO
  16. jmp period
复制代码

评分

1

查看全部评分

     
发表于 2025-2-21 22:01:09 | 显示全部楼层
本帖最后由 scoopydoo 于 2025-2-21 22:35 编辑

老兄专研得很深啊!

JMP 指令中的地址确实是绝对地址,但是当你调用 pio_add_program() 这个函数的时候,如果 offset 非负的话会调用一个 add_program_at_offset() 函数,并在其中对 JMP 指令做重定位。

  1. // these assert if unable
  2. int pio_add_program(PIO pio, const pio_program_t *program) {
  3.     uint32_t save = hw_claim_lock();
  4.     int offset = find_offset_for_program(pio, program);
  5.     if (offset >= 0) {
  6.         offset = add_program_at_offset(pio, program, (uint) offset);
  7.     }
  8.     hw_claim_unlock(save);
  9.     return offset;
  10. }
复制代码

  1. static int add_program_at_offset(PIO pio, const pio_program_t *program, uint offset) {
  2.     int rc = add_program_at_offset_check(pio, program, offset);
  3.     if (rc != 0) return rc;
  4.     for (uint i = 0; i < program->length; ++i) {
  5.         uint16_t instr = program->instructions[i];
  6.         pio->instr_mem[offset + i] = pio_instr_bits_jmp != _pio_major_instr_bits(instr) ? instr : instr + offset;
  7.     }
  8.     uint32_t program_mask = (1u << program->length) - 1;
  9.     _used_instruction_space[pio_get_index(pio)] |= program_mask << offset;
  10.     return (int)offset;
  11. }
复制代码

评分

1

查看全部评分

回复 支持 反对

使用道具 举报

     
 楼主| 发表于 2025-2-21 22:26:10 | 显示全部楼层
scoopydoo 发表于 2025-2-21 22:01
老兄专研的很多深啊!

JMP 指令中的地址确实是绝对地址,但是当你调用 pio_add_program() 这个函数的时 ...

老兄研究的才是真的深
看来加载时把我的程序目标码改了。
回复 支持 反对

使用道具 举报

     
发表于 2025-2-21 22:34:42 | 显示全部楼层
量子隧道 发表于 2025-2-21 22:26
老兄研究的才是真的深  
看来加载时把我的程序目标码改了。

俺没研究过,是看了你的帖子才去看代码的 ...
回复 支持 反对

使用道具 举报

     
 楼主| 发表于 2025-2-21 23:34:41 | 显示全部楼层
scoopydoo 发表于 2025-2-21 22:34
俺没研究过,是看了你的帖子才去看代码的 ...

那也说明您老体系清晰定位精准啊。
我猜这种改跳转代码或基地址加偏移地址的做法应该是操作系统或编译系统的常见做法。毕竟,很多场合是要在内存里动态加载目标码的。而跳转代码在编写的时候并不知道本程序的绝对位置,只知道在本程序中的跳转位置。
回复 支持 反对

使用道具 举报

     
发表于 2025-2-21 23:48:40 | 显示全部楼层
量子隧道 发表于 2025-2-21 23:34
那也说明您老体系清晰定位精准啊。
我猜这种改跳转代码或基地址加偏移地址的做法应该是操作系统或编译系 ...

确实是常规做法,好像叫做代码重定位 code relocation

评分

1

查看全部评分

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入会员

本版积分规则

小黑屋|手机版|矿石收音机 ( 蒙ICP备05000029号-1 )

蒙公网安备 15040402000005号

GMT+8, 2025-4-26 09:23

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表