きっかけ
前回、FTDIのUSB-UART変換で、100Kbps対応ができたが、やっぱり当初の目的だったRaspberry Pi(以下RPi)でも、non-standardなbaud rateを実現したい。
下調べ
ググる
前回も参照したこちらによると、init_uart_clock
でクロックの設定ができるらしい。が、sttyは
$ sudo stty -F /dev/ttyAMA0 100000
stty: invalid argument '100000'
と、失敗。
さらにこのinit_uart_clock
でググると、他にもinit_uart_baud
や、/boot/cmdline.txt(?)にbcm2708.uart_clock
追加、などの情報もある。
いろいろ試してオシロで測ったが、どれでやっても実際のbaud rateには変化が無い。
Raspbianは2016-02-26で、kernelは4.1だ。
sttyのbaud rate指定
sttyのbaud rateにはどんな値が設定できるのか。coreutilsのソースを読む。
mainのコマンドラインパーサーには、
else if (string_to_baud (arg) != (speed_t) -1)
とある。さらに、このstring_to_baud
は、
string_to_baud (const char *arg)
{
int i;
for (i = 0; speeds[i].string != NULL; ++i)
if (STREQ (arg, speeds[i].string))
return speeds[i].speed;
return (speed_t) -1;
}
となっており、speeds[i]
がポイントか。
static struct speed_map const speeds[] =
{
{"0", B0, 0},
{"50", B50, 50},
{"75", B75, 75},
(以下略)となっており、standardなbaud rateがテーブルになっているように見える。
kernelのbaud rate指定
static const speed_t baud_table[] = {
0, 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 2400, 4800,
9600, 19200, 38400, 57600, 115200, 230400, 460800,
(以下略)となっており、sttyと同じようなテーブルが定義されているように見える。
これでは、non-standardなbaud rateをstty経由でkernelに設定することは難しそうだ。
RPiのUARTドライバー
そもそも、clockを変えれば、実際のbaud rateに変化があってもいいじゃないか。君は何故変化が無いんだ。
/* Set baud rate */
writew(quot & 0x3f, port->membase + UART011_FBRD);
writew(quot >> 6, port->membase + UART011_IBRD);
で、レジスターにbaud rateを設定している。quot
は、
if (baud > port->uartclk/16)
quot = DIV_ROUND_CLOSEST(port->uartclk * 8, baud);
else
quot = DIV_ROUND_CLOSEST(port->uartclk * 4, baud);
で、baudに応じた適切な値が設定される。baudは、
/*
* Ask the core to calculate the divisor for us.
*/
baud = uart_get_baud_rate(port, termios, old, 0,
port->uartclk / clkdiv);
で、簡単に言うと、termiosからtty_termios_baud_rate()
で持ってきている。
つまり、kernelのcmdlineが何であれ、clockがどうであれ、誰かがsttyなどでbaud rateを指定した初期化を行ってしまえば、standardなbaud rateを元に、適切なレジスタ値が計算されてしまいそうな気がする。
昔は、/etc/inittabの修正などによって、ttyを初期化する人を抹殺して、kernelのcmdlineがそのまま/dev/ttyAMA0
に適用され続け、うまく行っていたのかもしれない(妄想)。ちなみにRaspbianは既にsystemdだった。
setserial
setserialによる設定
FTDIのドライバへは、setserialでnon-standardなbaud rateを設定できた。これは、どんな仕組みなのか。
if (ioctl(fd, TIOCSSERIAL, &new_serinfo) < 0) {
perror("Cannot set serial info");
exit(1);
}
となっており、TIOCSSERIAL
で設定される。
setserialによる設定取得
試しに、RPi上で、/dev/ttyAMA0
にsetserialで設定してみる。
$ setserial /dev/ttyAMA0
/dev/ttyAMA0, UART: undefined, Port: 0x0000, IRQ: 83
$ setserial /dev/ttyAMA0 spd_cust
$ setserial /dev/ttyAMA0
/dev/ttyAMA0, UART: undefined, Port: 0x0000, IRQ: 83, Flags: spd_cust
君は、spd_custを覚える能力があるではないか。FTDIのドライバーではなく、ARMのUARTドライバーなのに。
設定取得はどうなっているのか。kernelのTIOCGSERIAL
の実装を追う。
static void do_uart_get_info(struct tty_port *port,
struct serial_struct *retinfo)
{
struct uart_state *state = container_of(port, struct uart_state, port);
struct uart_port *uport = state->uart_port;
memset(retinfo, 0, sizeof(*retinfo));
:
retinfo->flags = uport->flags;
:
retinfo->baud_base = uport->uartclk / 16;
:
retinfo->custom_divisor = uport->custom_divisor;
(:=中略)flags
もcustom_divisor
も、struct uart_port
の情報を元にしていそうじゃないか。
つまり、struct uart_port
のflags
やcustom_divisor
なら、setserialから設定できそうじゃないか。しかも、uartclk
なんていうclockそのままっぽいメンバーも、struct uart_port
に居るじゃないか。
設計
- ユーザーは、前回と同じように、setserialからspd_custとdivisorを設定する
- UARTドライバーは、setserialから設定された値を
struct uart_port
で参照して、non-standardなbaud rateを計算する - non-standardなbaud rateを元に、既存のドライバーの処理で、レジスター値の計算等を行う
実装
/*
* Ask the core to calculate the divisor for us.
*/
baud = uart_get_baud_rate(port, termios, old, 0,
port->uartclk / clkdiv);
#if 1
if (baud == 38400 &&
((port->flags & ASYNC_SPD_MASK) == ASYNC_SPD_CUST) &&
(port->custom_divisor)) {
baud = port->uartclk / port->custom_divisor;
}
#endif
#if 1
〜#endif
が差分で、ftdi_sio.cのコードを元にしている。ただ、このブロックで参照できるのはstruct uart_port
なので、これにあわせてメンバーを調整している。
serial_core.cによると、baud_base = uport->uartclk / 16
なので、port->uartclk * 16
のほうが良いのかもしれないが、その時はdivisor
も16倍することになる。まぁこのままでいいや。
port->uartclk
には、一番最初に出てきたinit_uart_clock
の値が入っていることを期待している(ぶっつけ本番未確認)。
確認
kernelのbuild
本家にはこんなに丁寧なドキュメントがある。何も困らなかった。
init_uart_clockの変更
init_uart_clock=1600000
編集後のrebootで反映される模様。編集後にkernelを置き換えると、設定がリセットされてしまうようなので注意。
[ 0.132315] bcm2708.uart_clock = 1600000
設定
# stty -F /dev/ttyAMA0 38400
# setserial /dev/ttyAMA0 spd_cust divisor 16
# echo -n U > /dev/ttyAMA0
1600000 / 16 = 100000 = 100Kbps、になるはず…。
測定
蛇足
当初、devmem2で/dev/memをmmapし、ユーザー空間からレジスター設定しようと思ったが、設定手順や計算が面倒そうだったので、ドライバーに押し込んでしまった。