直接端口操作案例
随着编程技术日趋大众化,某些基本但有价值的代码可能会被抛在一边或者完全遗忘,例如直接端口操作 (DPM)。一些程序员认为,这些命令难以阅读,可能会使新手感到困惑,因此绝不应将其与更为人所熟悉的结构混合使用。然而,何不使用所有可用工具来保持代码简洁高效呢?
例如,一个 8 位微控制器的每个端口需要三个 8 位寄存器来控制端口活动。这些寄存器分别是:用于输入/输出方向的 DDRx,用于引脚逻辑电平控制的 PORTx,以及用于保存端口当前引脚状态的 PINx。为了详细说明,我们来看一下端口 C。在设置过程中,程序员可能希望将端口 C 引脚输出设置为初始低电平 (LOW) 状态。通常会编写一个恰当的“for 循环”,从 0 到 7 的每个引脚都需要重复这个 3 或 4 行代码,总共执行 32 行代码。在循环内部使用 pinMode() 和 digitalWrite() 之类的库函数会调用更多的“后台”活动,从而执行大量额外代码。
最终,端口 C 的八个方向寄存器位 (DDRC) 设置为高电平 (HIGH),而端口 C 的八个逻辑电平寄存器位 (PORTC) 设置为低电平 (LOW)。利用以下直接端口寄存器命令将位设置为 HIGH 或 LOW,可以实现完全相同的操作:
DDRC = 0xFF; //Set port C pins as OUTPUTS (in binary DDRD = 0b11111111;)
PORTC = 0x00; //Set port C pins LOW (in binary PORTC = 0b00000000;)
那么将端口 A 设置为输入呢?
DDRA = 0x00; //Set port A pins as INPUTS (in binary DDRA = 0b00000000;)
最后,将端口 D 设置为混合输入和输出:
DDRD = 0x0F; //Set the port D upper four bits as INPUTS and the lower as OUTPUTS (in binary DDRD = 0b00001111);
使用十六进制值(如 0xFF)可为理解寄存器中的位值分配提供一种有趣的思路。"0x" 为十六进制指示符,第一个数字 "F" 代表 8 位寄存器的高四位,第二个数字 "F" 代表低四位。
所有可能的位组合(用于设置 HIGH 或 LOW)都可以用十六进制或二进制文本(数字)表示。使用二进制文本更直观,因为在编写代码时可以看到所有八位。如果编译器支持,两者都是可接受的。不过,十六进制较短,看起来更酷。
备注:使用二进制文本不是 C/C++ 的通用标准。
更进一步
在设置中配置了端口 A 和 C 之后,只需在主循环中再加几行代码使用 DPM,便可读取数字设备并操作其他数字设备。实践中,使用极少量的寄存器命令和一个查找表,即可轻松对两台带行程限位开关并由数字操纵杆控制的步进电机进行编程。此方案的硬件设置如图 1 所示。
图 1:操纵杆/步进电机硬件设置,突出显示了微控制器端口 A 和 C。(图片来源:DigiKey)
硬件:
单个数字操纵杆(四个常开 (NO) 触点连接 VCC)连接端口 A 的引脚 0 至 3,微控制器的输出拉低。当触点闭合时,端口引脚变为 HIGH。这些触点分别代表上 (UP)、下 (DOWN)、左 (LEFT)、右 (RIGHT) 控制,两个相邻触点可以同时激活,因此共有八种可能的开关输出组合。第九种输出表示全停 (ALL STOP),即操纵杆居中且所有触点皆断开。
四个限位开关(NO 触点接地)连接端口 A 的其余引脚,分别对应操纵杆的 UP、DOWN、LEFT、RIGHT 配置。微控制器的开关输出拉高。当触点闭合时,这些引脚变为 LOW。
步进电机驱动器板通过三个控制引脚的高低电平转换来产生机械运动,以支持步进、方向和保持功能。对于此例,虽然有两个引脚不使用,但端口 C 的所有八位都用于操作两个驱动器。
编程:
为了生成正确的输出以提供给驱动器,可使用查找表来将四个操纵杆位转换为八个驱动器位。在主循环中,使用一行代码调用函数 "get_output()",并将 PINA 寄存器的内容发送至该函数。然后,将函数返回值直接写入 PORTC 寄存器:
PORTC = get_output(PINA);
该函数访问查找表 "lookup_output[ ]" 并返回索引值。除此之外,该函数中亦包含涉及限位开关的内容。"lookup_output[ ]" 方括号中包含的查找表索引值表示为移位和掩码表达式。所得的索引变量是输入寄存器的高四位(限位开关值)和低四位(操纵杆值)的按位与。如果任何限位开关触点闭合且高四位中存在零,则相应的低位清零。
备注:四个位共有 16 种唯一的位组合,因此可将 7 个未使用的查找表索引位置转换为 0x00 以防出错。
复制uint8_t get_output(uint8_t porta_val) { return lookup_output[(porta_val >> 4) & (porta_val & 0x0F)]; }
示例:
当操纵杆处于 UP 和 RIGHT 位置且所有限位开关均断开时,PINA 寄存器生成二进制值 0b11111001(或十六进制 0xF9)并发送至函数。函数使用 0b00001111 (0x0F) 的按位与运算将高四位清零,返回 0b00001001 (0x09) 作为查找表索引值。将另一按位与运算结果与限位开关状态移位值 0b00001111 (0x0F) 进行比较,则先前的值保持不变,返回 0b00001001 (0x09) 作为最终索引值,该值在查找表中指向 0b00100001 (0x21)。这是步进驱动器对 UP 和 RIGHT 的转换,请见图 2。
图 2:当操纵杆处于 UP 和 RIGHT 位置时,微控制器对端口 A 和端口 C 的输入解析。(图片来源:DigiKey)
如果触及 UP 限位开关,因而产生一个零位,则移位值将是 0b00000111 (0x07),而非 0b00001111 (0x0F),这将使相应的 UP 操纵杆值无效,导致最终索引值为 0b00000001 (0x01),而非 0b00001001 (0x09)。在查找表中,0b00000001 (0x01) 的转换值为 0x26。这是步进驱动器对仅限 RIGHT 的转换,请见图 3。
图 3:当操纵杆处于 UP 和 RIGHT 位置且触发 UP 限位开关时,微控制器对端口 A 和端口 C 的输入解析。(图片来源:DigiKey)
总结
无论程序员是否采用 DPM 作为配置、读取或写入端口数据的可行工具,大幅减少代码量都是一个很好的动力。若使用标准库函数,同样的操作会需要更多代码,并可能占用相当大的可用程序存储器空间。在使用 ATMEGA328P 微控制器的基准测试中,如下所示代码使用的存储器空间不到 1%。正确注释代码是理解代码编写中 DPM 功能的关键,并且有助于任何级别的程序员更轻松地调试。
DigiKey 硬件示例:
步进电机 - https://www.digikey.com/short/pdnfp4
步进控制器 - https://www.digikey.com/short/pdnf4r
操纵杆 - https://www.digikey.com/short/pdnf57
限位开关 - https://www.digikey.com/short/pdnfwm
样例代码:
复制const uint8_t lookup_output[16] = { 0x09, //Index 0 All Stop.Apply hold current 0x26, // Index 1 Right 0x34, // Index 2 Left 0x00, // Index 3 Unused 0x36, // Index 4 Down 0x0C, // Index 5 Down/Right 0x31, // Index 6 Down/Left 0x00, // Index 7 Unused 0x24, // Index 8 Up 0x21, // Index 9 Up/Right 0x0E, // Index 10 Up/Left 0x00, // Index 11 Unused 0x00, // Index 12 Unused 0x00, // Index 13 Unused 0x00, // Index 14 Unused 0x00 // Index 15 Unused }; void setup() { // Set all bits in port A direction register as INPUTs; // Limits (up, down, left, right) Joystick (up, down, left, right) DDRA = 0x00; // Set all bits of port C direction register as OUTPUTs; // Motor control (Not Used, Mot_1, Dir_1, En_1, Not Used, Mot_2, Dir_2, En_2 DDRC = 0xFF; } void loop() { //Send the port A values to the function.Write the return value to port C. PORTC = get_output(PINA); } /***** Input Value Translation Function *******/ uint8_t get_output(uint8_t porta_val) { // Compare the limit switch and joystick values.Retrieve and return the translated value.return lookup_output[(porta_val >> 4) & (porta_val & 0x0F)]; }
Have questions or comments? Continue the conversation on TechForum, Digi-Key's online community and technical resource.
Visit TechForum