Legacy Forum: Preserving Nearly 20 Years of Community History - A Time Capsule of Discussions, Memories, and Shared Experiences.

dmtalk

Bioloid robot kit from Korean company Robotis; CM5 controller block, AX12 servos..
18 postsPage 1 of 21, 2
18 postsPage 1 of 21, 2

dmtalk

Post by billyzelsnack » Sat Jan 27, 2007 7:43 am

Post by billyzelsnack
Sat Jan 27, 2007 7:43 am

I guess it's about time this got it's own named thread!

My write latency problem went away if I use sync writes. Here's the current dmtalk api functions.

int dmtalk_connect( enum DMTalkApiVersion apiVersion, const char* port );
void dmtalk_disconnect( );
int dmtalk_packet_read( int valuesCapacity, unsigned char* result_servoId, unsigned char* result_error, unsigned char* result_values );
int dmtalk_packet_write( int servoId, enum DMTalkInstruction instruction, enum DMTalkAddress address, int numValues, const unsigned char* values);
int dmtalk_ping( int servoId );
int dmtalk_value_get( int servoId, enum DMTalkAddress address, int numValues, unsigned char* result_values);
int dmtalk_value_get_8( int servoId, enum DMTalkAddress address, unsigned char* result_value);
int dmtalk_value_get_16( int servoId, enum DMTalkAddress address, unsigned short* result_value);
int dmtalk_value_set( int servoId, enum DMTalkAddress address, int dataLength, const unsigned char* data, int noRead);
int dmtalk_value_set_8( int servoId, enum DMTalkAddress address, unsigned char value, int noRead);
int dmtalk_value_set_16( int servoId, enum DMTalkAddress address, unsigned short value, int noRead);

int dmtalk_value_sync_set( int servoId, const unsigned char* data );
int dmtalk_value_sync_set_8( int servoId, unsigned char value );
int dmtalk_value_sync_set_16( int servoId, unsigned short value );
int dmtalk_value_sync_set_begin( enum DMTalkAddress address, int dataLength );
int dmtalk_value_sync_set_end();


I have not measured the latency (kinda hard with just writes), but it feels pretty instantaneous. I was getting a little discouraged with the performance, but I feel a lot better after seeing this. I'm actually surprised at how large the speedup was. It literally went from 1/4 second to 'pretty instantaneous'. That 1/4 second is even with changing the return level and tweaking the commtimeouts.

Just having fast writes is not too useful for my application. I want to be reading back the positions every pose (or possible more). Hopefully I won't have to get fancy and try to write a 'sync_read' concept for the communication between the cm5 and the pc!
I guess it's about time this got it's own named thread!

My write latency problem went away if I use sync writes. Here's the current dmtalk api functions.

int dmtalk_connect( enum DMTalkApiVersion apiVersion, const char* port );
void dmtalk_disconnect( );
int dmtalk_packet_read( int valuesCapacity, unsigned char* result_servoId, unsigned char* result_error, unsigned char* result_values );
int dmtalk_packet_write( int servoId, enum DMTalkInstruction instruction, enum DMTalkAddress address, int numValues, const unsigned char* values);
int dmtalk_ping( int servoId );
int dmtalk_value_get( int servoId, enum DMTalkAddress address, int numValues, unsigned char* result_values);
int dmtalk_value_get_8( int servoId, enum DMTalkAddress address, unsigned char* result_value);
int dmtalk_value_get_16( int servoId, enum DMTalkAddress address, unsigned short* result_value);
int dmtalk_value_set( int servoId, enum DMTalkAddress address, int dataLength, const unsigned char* data, int noRead);
int dmtalk_value_set_8( int servoId, enum DMTalkAddress address, unsigned char value, int noRead);
int dmtalk_value_set_16( int servoId, enum DMTalkAddress address, unsigned short value, int noRead);

int dmtalk_value_sync_set( int servoId, const unsigned char* data );
int dmtalk_value_sync_set_8( int servoId, unsigned char value );
int dmtalk_value_sync_set_16( int servoId, unsigned short value );
int dmtalk_value_sync_set_begin( enum DMTalkAddress address, int dataLength );
int dmtalk_value_sync_set_end();


I have not measured the latency (kinda hard with just writes), but it feels pretty instantaneous. I was getting a little discouraged with the performance, but I feel a lot better after seeing this. I'm actually surprised at how large the speedup was. It literally went from 1/4 second to 'pretty instantaneous'. That 1/4 second is even with changing the return level and tweaking the commtimeouts.

Just having fast writes is not too useful for my application. I want to be reading back the positions every pose (or possible more). Hopefully I won't have to get fancy and try to write a 'sync_read' concept for the communication between the cm5 and the pc!
billyzelsnack
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 618
Joined: Sat Dec 30, 2006 1:00 am

Post by billyzelsnack » Sat Jan 27, 2007 8:42 am

Post by billyzelsnack
Sat Jan 27, 2007 8:42 am

Not looking so good on getting those reads going in a reasonable amount of time.

I've lowered my commtimeouts as much as possible and still keep it working.
I've decreased the return delay way down to 10.
Amazingly I even got the baud rate working at 115k.

It's better, but the delay is still noticeable. Worse, I want more than just positions. Hopefully badwidth really is not the issue and grabbing position, speed, and load all at once does not hurt performance dramatically.

There is still a chance that my read code is broke and is always timing out. I should try an async version and see if that helps any.
Not looking so good on getting those reads going in a reasonable amount of time.

I've lowered my commtimeouts as much as possible and still keep it working.
I've decreased the return delay way down to 10.
Amazingly I even got the baud rate working at 115k.

It's better, but the delay is still noticeable. Worse, I want more than just positions. Hopefully badwidth really is not the issue and grabbing position, speed, and load all at once does not hurt performance dramatically.

There is still a chance that my read code is broke and is always timing out. I should try an async version and see if that helps any.
billyzelsnack
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 618
Joined: Sat Dec 30, 2006 1:00 am

Post by pepperm » Sat Jan 27, 2007 10:19 am

Post by pepperm
Sat Jan 27, 2007 10:19 am

Oops, just posted the following to your old thread....

Hi
What you are doing looks really good and you are clearly making progress.
Interestingly, Limor and I are trying to do something similar or at least
related. We are trying to make a Bioloid I/O module that would sit on the
1Mbit bus and provide extended I/O functionality to the Bioloid. I am
thinking of providing things like extra I/O to control whatever takes your
fancy or allows interfacing to other interface types such as i2C. Maybe the
unit could be used to get input from other sensor types such as a compass,
gyro or sonar unit. Details of the hardware are published here
http://robosavvy.com/modules.php?name=Forums&file=viewtopic&t=572&highlight=bioloid.

So far we have some code that is able to spot data being sent on the bus
but we are making very slow progress getting any further, and I was
wondering if you would be interested in helping out (if you have time that
is of course). Limor has some code in C and I have some in BASCOM AVR that
does the same thing. It just flashes an LED when data is seen on the bus.

What do you think?

Mark
Oops, just posted the following to your old thread....

Hi
What you are doing looks really good and you are clearly making progress.
Interestingly, Limor and I are trying to do something similar or at least
related. We are trying to make a Bioloid I/O module that would sit on the
1Mbit bus and provide extended I/O functionality to the Bioloid. I am
thinking of providing things like extra I/O to control whatever takes your
fancy or allows interfacing to other interface types such as i2C. Maybe the
unit could be used to get input from other sensor types such as a compass,
gyro or sonar unit. Details of the hardware are published here
http://robosavvy.com/modules.php?name=Forums&file=viewtopic&t=572&highlight=bioloid.

So far we have some code that is able to spot data being sent on the bus
but we are making very slow progress getting any further, and I was
wondering if you would be interested in helping out (if you have time that
is of course). Limor has some code in C and I have some in BASCOM AVR that
does the same thing. It just flashes an LED when data is seen on the bus.

What do you think?

Mark
pepperm
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 190
Joined: Sat Jul 01, 2006 1:00 am

Post by JonHylands » Sat Jan 27, 2007 1:58 pm

Post by JonHylands
Sat Jan 27, 2007 1:58 pm

What I'm planning on doing eventually is implement a new command: SYNC_READ.

The premise behind this command is the connection between the ATmega128 and the AX-12s is very fast and low latency, and the connection between your PC program and the ATmega128 might not be.

So basically, it will work like this:

PC issues SYNC_READ, with start address (same for all servos), length (same for all servos), and the IDs you want to poll.

ATmega128 gets the command, starts firing READ_DATA commands for each servo in the set, with the given start address and length. It stores the results in a RAM table it has set aside for this.

When the last servo is done, it sends the response back to the PC, all in one shot.

The numbers usually tell a lot of the story:

Reading 8 bytes per servo, 20 servos (READ_DATA): 14 bytes per servo response, for a total of 280 bytes

Using SYNC_READ for the same thing: 166 bytes

It really depends on how you have your PC connection set up. If your PC is using a USB interface to directly talk to the bus, then SYNC_READ won't help you at all. But if you've got a wireless link in there, and you have any kind of measurable latency in your link, SYNC_READ would be a big boost.

- Jon
What I'm planning on doing eventually is implement a new command: SYNC_READ.

The premise behind this command is the connection between the ATmega128 and the AX-12s is very fast and low latency, and the connection between your PC program and the ATmega128 might not be.

So basically, it will work like this:

PC issues SYNC_READ, with start address (same for all servos), length (same for all servos), and the IDs you want to poll.

ATmega128 gets the command, starts firing READ_DATA commands for each servo in the set, with the given start address and length. It stores the results in a RAM table it has set aside for this.

When the last servo is done, it sends the response back to the PC, all in one shot.

The numbers usually tell a lot of the story:

Reading 8 bytes per servo, 20 servos (READ_DATA): 14 bytes per servo response, for a total of 280 bytes

Using SYNC_READ for the same thing: 166 bytes

It really depends on how you have your PC connection set up. If your PC is using a USB interface to directly talk to the bus, then SYNC_READ won't help you at all. But if you've got a wireless link in there, and you have any kind of measurable latency in your link, SYNC_READ would be a big boost.

- Jon
JonHylands
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 512
Joined: Thu Nov 09, 2006 1:00 am
Location: Ontario, Canada

Post by billyzelsnack » Sun Jan 28, 2007 5:30 am

Post by billyzelsnack
Sun Jan 28, 2007 5:30 am

pepperm.

Sure. I'll help out. Looks like your plans are very much in line with mine. I basically want to hang sensors off the normal Dynamixel bus. Specifically I want to make an interface that'll accept a standard RC gyro.

My background is PC based software, but I am starting to get the hang of this uC stuff. As for circuit design. hahah. Let's just not talk about my progress with that!

JonHylands.

Yeah. I think sync read is going to be happening very soon in my code.

128*1024/8 = 16384 bytes per second / 18 servos = 910 bytes per second per servo / 50hz = 18 bytes @ 50hz

Maybe it's half that for bidirectional? Maybe it's div 9 for the 8n1? I dunno.

So the bandwidth is there or can easily get there for the PC<->CM5 (or whatever's in the middle) with a little dumb compression.

There must be something else going on besides bandwidth. Maybe my serial read code is broke (still looking into that) or something else in code land. OR.. Maybe there it just a large constant expense to tx/rx any packet. If that's the case, then that is where a sync read could really shine.
pepperm.

Sure. I'll help out. Looks like your plans are very much in line with mine. I basically want to hang sensors off the normal Dynamixel bus. Specifically I want to make an interface that'll accept a standard RC gyro.

My background is PC based software, but I am starting to get the hang of this uC stuff. As for circuit design. hahah. Let's just not talk about my progress with that!

JonHylands.

Yeah. I think sync read is going to be happening very soon in my code.

128*1024/8 = 16384 bytes per second / 18 servos = 910 bytes per second per servo / 50hz = 18 bytes @ 50hz

Maybe it's half that for bidirectional? Maybe it's div 9 for the 8n1? I dunno.

So the bandwidth is there or can easily get there for the PC<->CM5 (or whatever's in the middle) with a little dumb compression.

There must be something else going on besides bandwidth. Maybe my serial read code is broke (still looking into that) or something else in code land. OR.. Maybe there it just a large constant expense to tx/rx any packet. If that's the case, then that is where a sync read could really shine.
billyzelsnack
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 618
Joined: Sat Dec 30, 2006 1:00 am

Post by JonHylands » Sun Jan 28, 2007 2:00 pm

Post by JonHylands
Sun Jan 28, 2007 2:00 pm

One of the things I'm working on in the next week is my 5-axis IMU. This will be a tiny board, that stacks under the Sparkfun 5-axis IMU board, with an ATmega168 on it. The 168 will get input from the IMU board via 6 A/D converters, and it will talk on the Bioloid bus at 1.0 Mbps as a slave device, just like anything else.

The IMU will be powered off the bus, so it will literally be a bus device.

My brother is puttng together a slave framework for parsing bus commands that will run on the 168. I'm going to finish that, and add in the IMU-specific code. I'm going to publish the whole thing once I'm done.

Doing a bus device to run a standard hobby servo would be pretty straight forward, I think...

- Jon
One of the things I'm working on in the next week is my 5-axis IMU. This will be a tiny board, that stacks under the Sparkfun 5-axis IMU board, with an ATmega168 on it. The 168 will get input from the IMU board via 6 A/D converters, and it will talk on the Bioloid bus at 1.0 Mbps as a slave device, just like anything else.

The IMU will be powered off the bus, so it will literally be a bus device.

My brother is puttng together a slave framework for parsing bus commands that will run on the 168. I'm going to finish that, and add in the IMU-specific code. I'm going to publish the whole thing once I'm done.

Doing a bus device to run a standard hobby servo would be pretty straight forward, I think...

- Jon
JonHylands
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 512
Joined: Thu Nov 09, 2006 1:00 am
Location: Ontario, Canada

Post by billyzelsnack » Mon Jan 29, 2007 3:21 am

Post by billyzelsnack
Mon Jan 29, 2007 3:21 am

I'm thinking of coding up something a little different than a sync read. It could be thought of more as an auto sync read I guess.

The idea is that you send a command from the PC with everything that you want and an update interval. The cm5 will then grab all that data from the servos and send it to the PC with no more read requests from the PC.

This gets rid of some latency and bandwidth. For my application I would be sending the same packets over and over again, so why even bother. The rest of the api will still be there for one off requests, but this would be used most of the time.
I'm thinking of coding up something a little different than a sync read. It could be thought of more as an auto sync read I guess.

The idea is that you send a command from the PC with everything that you want and an update interval. The cm5 will then grab all that data from the servos and send it to the PC with no more read requests from the PC.

This gets rid of some latency and bandwidth. For my application I would be sending the same packets over and over again, so why even bother. The rest of the api will still be there for one off requests, but this would be used most of the time.
billyzelsnack
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 618
Joined: Sat Dec 30, 2006 1:00 am

Post by JonHylands » Mon Jan 29, 2007 4:51 am

Post by JonHylands
Mon Jan 29, 2007 4:51 am

I have thought about what you are talking about, with the auto-sync read. The only downside I see to it is the PC then has to be very careful about how it communicates with the bus, because you really want to send data down on the bus at least as often as you get data back again.

Since the PC is not typically a hard real-time device, getting the timing correct (to avoid contention between the PC and the ATmega128) could prove problematic.

- Jon
I have thought about what you are talking about, with the auto-sync read. The only downside I see to it is the PC then has to be very careful about how it communicates with the bus, because you really want to send data down on the bus at least as often as you get data back again.

Since the PC is not typically a hard real-time device, getting the timing correct (to avoid contention between the PC and the ATmega128) could prove problematic.

- Jon
JonHylands
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 512
Joined: Thu Nov 09, 2006 1:00 am
Location: Ontario, Canada

Post by billyzelsnack » Mon Jan 29, 2007 7:16 am

Post by billyzelsnack
Mon Jan 29, 2007 7:16 am

Why would there be contention? You've got separate TX/RX wires between the PC and uC.

I also am not so sure about a 1 to 1 send/receive of data. I think I'd prefer doing more reads than writes if I can sustain a any reasonable frames per second. Meaning.. I'd rather have 30 reads and 20 writes than 25 reads and 25 writes per second. I think that I might even not care so much about writes above say 50hz, but I'd sure take the ability to have reads at 100hz and beyond.
Why would there be contention? You've got separate TX/RX wires between the PC and uC.

I also am not so sure about a 1 to 1 send/receive of data. I think I'd prefer doing more reads than writes if I can sustain a any reasonable frames per second. Meaning.. I'd rather have 30 reads and 20 writes than 25 reads and 25 writes per second. I think that I might even not care so much about writes above say 50hz, but I'd sure take the ability to have reads at 100hz and beyond.
billyzelsnack
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 618
Joined: Sat Dec 30, 2006 1:00 am

Post by JonHylands » Mon Jan 29, 2007 1:34 pm

Post by JonHylands
Mon Jan 29, 2007 1:34 pm

You have contention because you can't both read & write at the same time. The only serial protocol I've ever heard of that allows that is SPI.

If both the PC and the ATmega128 decide to start transmitting at the same time, you're going to have problems...

I2C has a really nice way of dealing with that (multi-master), with a simple bus arbitration scheme built into the hardware that chooses the winner when there is a conflict.

RS-232 has no such niceness...

- Jon
You have contention because you can't both read & write at the same time. The only serial protocol I've ever heard of that allows that is SPI.

If both the PC and the ATmega128 decide to start transmitting at the same time, you're going to have problems...

I2C has a really nice way of dealing with that (multi-master), with a simple bus arbitration scheme built into the hardware that chooses the winner when there is a conflict.

RS-232 has no such niceness...

- Jon
JonHylands
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 512
Joined: Thu Nov 09, 2006 1:00 am
Location: Ontario, Canada

Post by billyzelsnack » Mon Jan 29, 2007 4:39 pm

Post by billyzelsnack
Mon Jan 29, 2007 4:39 pm

That's a good thing to know. I just assumed that you could because they each had their own wire. What's the point of them having their own wires?
That's a good thing to know. I just assumed that you could because they each had their own wire. What's the point of them having their own wires?
billyzelsnack
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 618
Joined: Sat Dec 30, 2006 1:00 am

Post by JonHylands » Mon Jan 29, 2007 5:03 pm

Post by JonHylands
Mon Jan 29, 2007 5:03 pm

The reason they each have their own wires is to make it electrically simpler to handle. It also allows a certain amount of multi-master, although there are no guarantees that it will work.

The reason it makes more sense for the PC to be the master is then there are no timing/conflict issues.

If you really wanted to do it that way, you could have the CM-5 send a special "notification packet" to the PC that would signal the PC that it was time for it to send position/speed values. The CM-5 would then have to wait until it got the data from the PC before sending anything else.

If you're running a 115K link between the PC and the CM-5, you're going to be limited with how often you can send sensor data back. Using my previous estimate of 166 bytes per poll, that is 1660 bits per cycle. Your absolute theoretical maximum in that case would be around 69 cycles per second, and that's assuming you never send anything to the servos, only getting data from them.

Basically, in real terms, you've got about 50 transactions per second, and you need to divide that up into sends and receives. You can increase that number by getting less data from each servo. My estimates are based on getting 8 bytes per servo, which includes position, speed, load, voltage, and temperature. You could skip the last two, and reduce the total transfer by 40 bytes, but you're still in the same general ballpark.

Sending and receiving the "minimum" reasonable amount of data, you're looking at about 40 cycles per second, if you want load information from the servos. That's going to completely saturate your serial connection.

These timing limitations are the main reason I am building a 1.0 Mbps wireless connection between the PC and the bus.

- Jon
The reason they each have their own wires is to make it electrically simpler to handle. It also allows a certain amount of multi-master, although there are no guarantees that it will work.

The reason it makes more sense for the PC to be the master is then there are no timing/conflict issues.

If you really wanted to do it that way, you could have the CM-5 send a special "notification packet" to the PC that would signal the PC that it was time for it to send position/speed values. The CM-5 would then have to wait until it got the data from the PC before sending anything else.

If you're running a 115K link between the PC and the CM-5, you're going to be limited with how often you can send sensor data back. Using my previous estimate of 166 bytes per poll, that is 1660 bits per cycle. Your absolute theoretical maximum in that case would be around 69 cycles per second, and that's assuming you never send anything to the servos, only getting data from them.

Basically, in real terms, you've got about 50 transactions per second, and you need to divide that up into sends and receives. You can increase that number by getting less data from each servo. My estimates are based on getting 8 bytes per servo, which includes position, speed, load, voltage, and temperature. You could skip the last two, and reduce the total transfer by 40 bytes, but you're still in the same general ballpark.

Sending and receiving the "minimum" reasonable amount of data, you're looking at about 40 cycles per second, if you want load information from the servos. That's going to completely saturate your serial connection.

These timing limitations are the main reason I am building a 1.0 Mbps wireless connection between the PC and the bus.

- Jon
JonHylands
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 512
Joined: Thu Nov 09, 2006 1:00 am
Location: Ontario, Canada

Post by DerekZahn » Mon Jan 29, 2007 5:31 pm

Post by DerekZahn
Mon Jan 29, 2007 5:31 pm

Sorry to jump in here as I do not have a Bioloid, but I'm not quite understanding the limitation you're talking about Jon. Is it a hardware or software limitation on the CM-5? I use an RS-232 bus on Bing and every node (including the PC) is simultaneously sending and receiving data at 115k baud. That way my packets only incur one byte of latency through each node. The RX and TX paths are completely distinct (different pins, different hardware on the processor UARTs, etc) so there is no reason for it not to work, and I don't see any problems. There is no "master" as such, each connection of RX-TX or TX-RX is completely distinct and requires no synchronization with the other path.

Is the CM-5 hardware unusually badly designed so that it can only do half duplex?
Sorry to jump in here as I do not have a Bioloid, but I'm not quite understanding the limitation you're talking about Jon. Is it a hardware or software limitation on the CM-5? I use an RS-232 bus on Bing and every node (including the PC) is simultaneously sending and receiving data at 115k baud. That way my packets only incur one byte of latency through each node. The RX and TX paths are completely distinct (different pins, different hardware on the processor UARTs, etc) so there is no reason for it not to work, and I don't see any problems. There is no "master" as such, each connection of RX-TX or TX-RX is completely distinct and requires no synchronization with the other path.

Is the CM-5 hardware unusually badly designed so that it can only do half duplex?
DerekZahn
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 141
Joined: Wed Mar 16, 2005 1:00 am
Location: Boulder CO, USA

Post by JonHylands » Mon Jan 29, 2007 5:39 pm

Post by JonHylands
Mon Jan 29, 2007 5:39 pm

No, there isn't anything funny about the CM-5, its just an ATmega128.

I guess I may stand corrected, but I'll be interested in hearing from an ATmega128 expert on whether the hardware UART can handle that.

You're using LPC2138's for the nodes on Bing, which is a far more powerful processor than the ATmega128.

- Jon
No, there isn't anything funny about the CM-5, its just an ATmega128.

I guess I may stand corrected, but I'll be interested in hearing from an ATmega128 expert on whether the hardware UART can handle that.

You're using LPC2138's for the nodes on Bing, which is a far more powerful processor than the ATmega128.

- Jon
JonHylands
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 512
Joined: Thu Nov 09, 2006 1:00 am
Location: Ontario, Canada

Post by DerekZahn » Mon Jan 29, 2007 5:45 pm

Post by DerekZahn
Mon Jan 29, 2007 5:45 pm

I see. I'd be astonished if an ATMega128 couldn't handle it (with the proper software support to keep both data paths busy), since this is exactly what "full duplex" means, but I have never tried it on an Atmel chip so I can't say for sure.
I see. I'd be astonished if an ATMega128 couldn't handle it (with the proper software support to keep both data paths busy), since this is exactly what "full duplex" means, but I have never tried it on an Atmel chip so I can't say for sure.
DerekZahn
Savvy Roboteer
Savvy Roboteer
User avatar
Posts: 141
Joined: Wed Mar 16, 2005 1:00 am
Location: Boulder CO, USA

Next
18 postsPage 1 of 21, 2
18 postsPage 1 of 21, 2