在NumWorks计算器中添加Raspberry Pi

创意展示、DIY分享、经验交流
回复
头像
shaoziyang
帖子: 3917
注册时间: 2019年 10月 21日 13:48

在NumWorks计算器中添加Raspberry Pi

#1

帖子 shaoziyang »

来自:https://zardam.github.io/post/raspberrypi-numworks/

Raspberry Pi Zero放在我的桌子上有长时间了,还在寻找与此有关的东西。我还想学习STM32,主要是DMA和中断方面。由于NumWorks计算器由STM32F412驱动,那为什么不将它们放在一起呢?



想法是在计算器上添加一个应用程序,该应用程序将显示Raspberry Pi的输出,并从键盘向其发送按键数据。

我已经在Raspberry Pi上通过 SPI 驱动 fbtft 进行显示,并且 NumWorks 的一些裸露的焊盘包含了SPI总线,因此这应该很容易!

图片


显示来自SPI的数据

计算器固件已经完成了所有艰苦的底层工作(初始化显示),并提供了用于控制显示的API。实际上,它是由FSMC(柔性静态内存控制器)驱动的,因此从CPU的角度来看,该显示可通过两个静态地址访问,一个用于命令,一个用于数据,宽度为16位。

对于此应用程序,唯一需要的命令是设置像素显示区域的命令。幸运的是,它已经在固件中实现。推入像素就像将每个像素依次写入数据地址一样简单,并且它们将像在标准监视器中一样从左到右,从上到下显示。

因此,只需要将像素从SPI控制器复制到显示地址。对于DMA引擎来说,像这样复制像素是一件容易的事。

窗口的设置是在每一帧之前完成的,使用SPI总线中未使用的的MISO引脚作为软件芯片选择。因此,当MISO变低时,将触发一个中断,设置SPI控制器上的软件芯片选择(以接受传入数据),并在显示控制器中配置窗口,覆盖整个屏幕。

设置窗口大约需要3 µs,因此来自DMA的第一个字必须在此延迟后到达。在Raspberry Pi端,芯片选择和第一个输入字节之间有足够的延迟(大约10 µs)。


设置GPIO

SPI引脚必须配置为第二功能模式,以便在内部连接到SPI控制器。MISO引脚保留为普通GPIO,因为它用于触发中断。

GPIOA.MODER()->setMode(5, GPIO::MODER::Mode::AlternateFunction);
GPIOA.AFR()->setAlternateFunction(5, GPIO::AFR::AlternateFunction::AF5);
GPIOA.MODER()->setMode(6, GPIO::MODER::Mode::Input);
GPIOA.MODER()->setMode(7, GPIO::MODER::Mode::AlternateFunction);
GPIOA.AFR()->setAlternateFunction(7, GPIO::AFR::AlternateFunction::AF5);



设置SPI控制器

SPI控制器的配置非常简单。它设置为16位模式,即RXONLY(因为MISO引脚被复用于芯片选择)和软件芯片选择。

SPI1.CR1()->setRXONLY(true);
SPI1.CR1()->setSSI(true); // Software chip select
SPI1.CR1()->setSSM(true); // Software chip select mode
SPI1.CR1()->setDFF(true); // 16 bits
SPI1.CR1()->setSPE(true); // enable



设置DMA控制器

为了使DMA工作,需要选择正确的DMA控制器,流和通道。然后配置源地址(此处为SPI1数据寄存器),目标地址(显示控制器数据地址),源和目标数据宽度(16位),模式(循环)以及编号要传送的数据(此处为1,因为我们处于循环模式)。无需增加源/目标地址,它始终是相同的。

DMAEngine.SPAR(DMAStream)->set((uint32_t)SPI1.DR()); // Source
DMAEngine.SM0AR(DMAStream)->set((uint32_t)Ion::Display::Device::DataAddress); // Destination
DMAEngine.SNDTR(DMAStream)->set(1); // Number of items
DMAEngine.SCR(DMAStream)->setCHSEL(3); // SPI Channel
DMAEngine.SCR(DMAStream)->setDIR(DMA::SCR::Direction::PeripheralToMemory);
DMAEngine.SCR(DMAStream)->setMSIZE(DMA::SCR::DataSize::HalfWord);
DMAEngine.SCR(DMAStream)->setPSIZE(DMA::SCR::DataSize::HalfWord);
DMAEngine.SCR(DMAStream)->setCIRC(true); // Circular
DMAEngine.SCR(DMAStream)->setEN(true); // Enable



设置SPI控制器以发出DMA请求

只需通过使能SPI控制寄存器中的RXDMAEN位即可完成。

SPI1.CR2()->setRXDMAEN(true); // enable DMA requests


通过MISO引脚设置中断

这是棘手的部分。引脚和实际的中断处理程序之间有多个抽象级别。

首先,必须将EXTI(外部中断/事件控制器)配置为触发NVIC(嵌套矢量中断控制器)中的中断线。

然后,必须启用NVIC线,并定义相应的中断处理程序。不要忘记在处理程序中确认中断!

SYSCFG.EXTICR2()->setEXTI(Ion::Rpi::Device::ChipSelectPin, Ion::Rpi::Device::ChipSelectGPIO);
EXTI.RTSR()->set(Ion::Rpi::Device::ChipSelectPin, true);
EXTI.FTSR()->set(Ion::Rpi::Device::ChipSelectPin, true);
NVIC.NVIC_ISER0()->set(23, true);



中断处理程序

它有两件事:
  • 当CS变低时,它将激活SPI控制器的软件芯片选择,然后触发显示控制器中窗口的配置。
  • 当CS变高时,它会禁用SPI控制器的软件芯片选择。SPI上收到的任何数据都将被丢弃。
void rpi_isr() {
EXTI.PR()->set(Ion::Rpi::Device::ChipSelectPin, true);

if(GPIOA.IDR()->get(6)) {
SPI1.CR1()->setSSI(true);
} else {
Ion::Display::Device::setDrawingArea(KDRect(0,0,320,240), Ion::Display::Device::Orientation::Landscape);
*Ion::Display::Device::CommandAddress = Ion::Display::Device::Command::MemoryWrite;
SPI1.CR1()->setSSI(false);
}
}



局限性

如果出现问题,则不会进行错误处理,因此整个链都可能被阻塞。这主要发生在SPI控制器上。如果DMA读取数据的速度不够快,它将被卡住,等待错误的确认。


通过SPI总线发送Raspberry Pi显示

最初,我计划按原样使用fbtft,但是在深入研究代码之后,它似乎不能这样使用,因为它直接访问显示控制器,以便优化像素的推送(通过限制到屏幕上已更改的区域)。我不想在计算器上实现这种功能,因此决定自己编写。

通过使用来自fbtft的概念和代码,fbtft是Sprite_tm编写的另一个驱动程序,也是内核内的vfb驱动程序,我组装了一个“quick and dirty” linux模块,该模块能满足我的需要:将整个帧缓冲区推到SPI总线。

显示器为320x240像素,每个16bpp,因此每帧为1228800位。STM32F412的最大SPI频率为50 MHz,但Raspberry Pi无法精确产生它。经过测试使用62.5 MHz,它似乎运行良好,因此,理论上最大帧速率为(1228800/(62.5x106))−1 ≈ 50 fps.


实施键盘

这是计算器上应用程序的“活动”部分(因为SPI和DMA在后台运行)。计算器只是通过UART将固件的键盘扫描例程的结果(64位位域)发送到RPi。在RPi端,守护程序在UART上侦听,并使用uinput生成内核的密钥代码

映射有点乏味,并且在Linux端使用自定义键映射可能会更好。我没有采用这种方式,因为仍然可以使用外部蓝牙键盘(而且我不知道是否可以在多个键盘上使用不同的键盘映射)。

计算器键盘只有46个键,因此为了映射足够的键,按钮“ x,n,t”和“ var”用于在标准键和数字之间切换。并非标准键盘的所有键都被映射。这一点值得加强……

鼠标仅依赖于X.Org鼠标仿真。按下电源按钮即可触发。

Calc          Keymap 1          Keymap 2

{"left",      {KEY_KP4,         KEY_KP4}},
{"up",        {KEY_KP8,         KEY_KP8}},
{"down",      {KEY_KP2,         KEY_KP2}},
{"right",     {KEY_KP6,         KEY_KP6}},
{"ok",        {BTN_LEFT,        BTN_LEFT}},
{"back",      {BTN_RIGHT,       BTN_RIGHT}},
{"home"},     // not handled here
{"power"},    // toggle mouse mode
{NULL},
{NULL},
{NULL},
{NULL},
{"shift",     {KEY_LEFTSHIFT,   KEY_LEFTSHIFT}},
{"alpha",     {KEY_CAPSLOCK,    KEY_CAPSLOCK}},
{"xnt"},      // Switch to first keymap
{"var"},      // Switch to second keymap
{"toolbox",   {KEY_RIGHTALT,    KEY_RIGHTALT}},
{"backspace", {KEY_BACKSPACE,   KEY_ESC}},
{"A",         {KEY_Q,           KEY_F1}},
{"B",         {KEY_B,           KEY_F2}},
{"C",         {KEY_C,           KEY_F3}},
{"D",         {KEY_D,           KEY_F4}},
{"E ,",       {KEY_E,           KEY_F5}},
{"F",         {KEY_F,           KEY_F6}},
{"G",         {KEY_G,           KEY_F7}},
{"H",         {KEY_H,           KEY_F8}},
{"I",         {KEY_I,           KEY_F9}},
{"J",         {KEY_J,           KEY_F10}},
{"K",         {KEY_K,           KEY_F11}},
{"L",         {KEY_L,           KEY_F12}},
{"M 7",       {KEY_SEMICOLON,   KEY_7}},
{"N 8",       {KEY_N,           KEY_8}},
{"O 9",       {KEY_O,           KEY_9}},
{"P (",       {KEY_P,           KEY_5}},
{"Q )",       {KEY_A,           KEY_MINUS}},
{NULL},
{"R 4",       {KEY_R,           KEY_4}},
{"S 5",       {KEY_S,           KEY_5}},
{"T 6",       {KEY_T,           KEY_6}},
{"U *",       {KEY_U,           KEY_KPASTERISK}},
{"V /",       {KEY_V,           KEY_KPSLASH}},
{NULL},
{"W 1",       {KEY_Z,           KEY_1}},
{"X 2",       {KEY_X,           KEY_2}},
{"Y 3",       {KEY_Y,           KEY_3}},
{"Z +",       {KEY_W,           KEY_KPPLUS}},
{"space -",   {KEY_SPACE,       KEY_KPMINUS}},
{NULL},
{"? 0",       {KEY_M,           KEY_0}},
{"! .",       {KEY_COMMA,       KEY_COMMA}},
{"x10^x",     {KEY_LEFTCTRL,    KEY_LEFTCTRL}},
{"ans",       {KEY_LEFTALT,     KEY_LEFTALT}},
{"exe",       {KEY_ENTER,       KEY_EQUAL}},



树莓派

给Pi上电

我最初使用的是 RPi zero(不带无线)。当以2.8 V(计算器上的内部稳压电压)供电时,它似乎工作良好,并且板上配备了SD卡读卡器和一个晶体管来控制其电源。我决定重新使用SD电源板来控制RPi的电源。

图片

但之后我意识到,不包括WiFi令人难过,所以我订购了“W”的版本。事实证明,对2.8 V不能稳定运行,WiFi芯片的数据手册指出它至少需要3V。禁用WiFi芯片(config.txt中的“ dtoverlay = pi3-disable-wifi”)可以使RPi正常工作在2.8 V。

因此,我决定直接通过电池为RPi供电。由于无法再使用SD卡的占用空间,因此我在SD推车插槽占用空间的未连接引脚上焊接了晶体管,并以“自由型”方式上拉了电阻。

我使用了一个NTR1P02LT1和一个10kΩ电阻,但是任何能够处理至少100 mA电流的P通道 MOSFET都应该不错。

图片

电压水平没有问题,因为STM32上使用的所有引脚都可承受5 V电压。

RPi会在进入应用程序时启动,并在计算器关闭电源时关闭。因此,可以根据需要离开或进入RPi应用程序。


放入计算器

RPi非常适合放在计算器中。RPi的连接器所在的位置没有组件。我在HDMI接口和计算器的显示接口上用双面胶将其固定在原位。

不幸的是,这有点太厚了,无法直接用原来的后盖(切掉了垂直凸片),但是可以将它留在原处:

图片

图片

图片


软件配置

树莓派

GitHub存储库:https : //github.com/zardam/spifb

仅需要安装内核头文件,编译,安装和自动加载模块。

sudo apt-get install raspberrypi-kernel-headers build-essential
git clone https://github.com/zardam/spifb.git
cd spifb
make -C /lib/modules/$(uname -r)/build M=$PWD
sudo make -C /lib/modules/$(uname -r)/build M=$PWD modules_install
sudo depmod -a


/etc/modules

spi-bcm2835
spifb
uinput


/boot/config.txt

dtparam=spi=on

# Disable HDMI output, saves some power
hdmi_blanking=2

# Enable the mini uart (/dev/ttyS0 on a PI Zero W)
enable_uart=1

# Disable LED, saves some power
dtparam=act_led_trigger=none
dtparam=act_led_activelow=on


然后有两种可能性:
  • 直接使用帧缓冲区。这是最简单的方法,但是RPi GPU的硬件加速将不可用
  • 使用fbcp将普通帧缓冲区(fb0)复制到SPI帧缓冲区(spi1)。该副本会带来一些CPU开销,但是可以使用硬件加速,并且可以缩放帧缓冲区,因为320x240的分辨率几乎无法使用。
直接使用帧缓冲区

配置与使用fbtft相同。

/boot/cmdline.txt

fbcon=map:10

X服务器

sudo apt-get install xserver-xorg-video-fbdev

/usr/share/X11/xorg.conf.d/99-fbdev.conf

Section "Device"  
  Identifier "myfb"
  Driver "fbdev"
  Option "fbdev" "/dev/fb1"
EndSection


fbcp

我用了这个分支。需要CMake来构建它。

sudo apt-get install cmake
git clone https://github.com/Oper8or/rpi-fbcp.git
cd rpi-fbcp
mkdir build
cd build
cmake ..
make


/boot/config.txt

hdmi_force_hotplug=1
hdmi_cvt=640 480 60 1 0 0 0
hdmi_group=2
hdmi_mode=87


/etc/systemd/system/fbcp.service

[Unit]
Description=NumWorks input device
After=systemd-modules-load.service

[Service]
Type=simple
WorkingDirectory=/home/pi/rpi-fbcp/build
ExecStart=/home/pi/rpi-fbcp/build/fbcp
User=root
Group=root
Restart=on-failure

[Install]
WantedBy=multi-user.target


然后,启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl enable fbcp
sudo systemctl start fbcp


键盘

GitHub存储库:https : //github.com/zardam/uinput-serial-keyboard

git clone https://github.com/zardam/uinput-serial-keyboard
cd uinput-serial-keyboard
gcc uinput.c -o uinput


需要在lxde会话配置中禁用lxkeymap(只需使用GUI工具)。

必须禁用linux串行控制台。在/boot/cmdline.txt中,删除:

console=serial0,115200

/etc/systemd/system/nwinput.service

[Unit]
Description=NumWorks input device

[Service]
Type=simple
WorkingDirectory=/home/pi/uinput-serial-keyboard/
ExecStart=/home/pi/uinput-serial-keyboard/uinput
User=root
Group=root
Restart=on-failure

[Install]
WantedBy=multi-user.target


然后,启用并启动服务:

sudo systemctl daemon-reload
sudo systemctl enable nwinput
sudo systemctl start nwinput


计算器

GitHub存储库:https : //github.com/zardam/epsilon/tree/rpi

在计算机上,安装NumWorks SDK之后:

git clone -b rpi https://github.com/zardam/epsilon.git
cd epsilon
make epsilon_flash


然后连接并重置计算器,以刷新自定义固件。

完成;)

在计算器的浏览器中运行NumWorks模拟器:

图片

还有很多改进的空间,一些地方做得不够完善,但这仍然是一个非常有趣的项目。
 

回复

  • 随机主题
    回复总数
    阅读次数
    最新文章